qrcode-vue3
Version:
Add a style and an image to your QR code Vue3
471 lines (395 loc) • 14.9 kB
text/typescript
/* eslint-disable no-throw-literal */
import calculateImageSize from "../tools/calculateImageSize";
import errorCorrectionPercents from "../constants/errorCorrectionPercents";
import QRDot from "./QRDot";
import QRCornerSquare from "./QRCornerSquare";
import QRCornerDot from "./QRCornerDot";
import type { RequiredOptions, Gradient } from "./QROptions";
import gradientTypes from "../constants/gradientTypes";
import type { QRCode } from "../types";
type FilterFunction = (i: number, j: number) => boolean;
const squareMask = [
[1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1]
];
const dotMask = [
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]
];
export default class QRCanvas {
_canvas: HTMLCanvasElement;
_options: RequiredOptions;
_qr?: QRCode;
_image?: HTMLImageElement;
// TODO don't pass all options to this class
constructor(options: RequiredOptions) {
this._canvas = document.createElement("canvas");
this._canvas.width = options.width;
this._canvas.height = options.height;
this._options = options;
}
get context(): CanvasRenderingContext2D | null {
return this._canvas.getContext("2d");
}
get width(): number {
return this._canvas.width;
}
get height(): number {
return this._canvas.height;
}
getCanvas(): HTMLCanvasElement {
return this._canvas;
}
clear(): void {
const canvasContext = this.context;
if (canvasContext) {
canvasContext.clearRect(0, 0, this._canvas.width, this._canvas.height);
}
}
async drawQR(qr: QRCode): Promise<void> {
const count = qr.getModuleCount();
const minSize = Math.min(this._options.width, this._options.height) - this._options.margin * 2;
const dotSize = Math.floor(minSize / count);
let drawImageSize = {
hideXDots: 0,
hideYDots: 0,
width: 0,
height: 0
};
this._qr = qr;
if (this._options.image) {
await this.loadImage();
if (!this._image) return;
const { imageOptions, qrOptions } = this._options;
const coverLevel = imageOptions.imageSize * errorCorrectionPercents[qrOptions.errorCorrectionLevel];
const maxHiddenDots = Math.floor(coverLevel * count * count);
drawImageSize = calculateImageSize({
originalWidth: this._image.width,
originalHeight: this._image.height,
maxHiddenDots,
maxHiddenAxisDots: count - 14,
dotSize
});
}
this.clear();
this.drawBackground();
this.drawDots((i: number, j: number): boolean => {
if (this._options.imageOptions.hideBackgroundDots) {
if (
i >= (count - drawImageSize.hideXDots) / 2 &&
i < (count + drawImageSize.hideXDots) / 2 &&
j >= (count - drawImageSize.hideYDots) / 2 &&
j < (count + drawImageSize.hideYDots) / 2
) {
return false;
}
}
if (squareMask[i]?.[j] || squareMask[i - count + 7]?.[j] || squareMask[i]?.[j - count + 7]) {
return false;
}
if (dotMask[i]?.[j] || dotMask[i - count + 7]?.[j] || dotMask[i]?.[j - count + 7]) {
return false;
}
return true;
});
this.drawCorners();
if (this._options.image) {
this.drawImage({ width: drawImageSize.width, height: drawImageSize.height, count, dotSize });
}
}
drawBackground(): void {
const canvasContext = this.context;
const options = this._options;
if (canvasContext) {
if (options.backgroundOptions.gradient) {
const gradientOptions = options.backgroundOptions.gradient;
const gradient = this._createGradient({
context: canvasContext,
options: gradientOptions,
additionalRotation: 0,
x: 0,
y: 0,
size: this._canvas.width > this._canvas.height ? this._canvas.width : this._canvas.height
});
gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
gradient.addColorStop(offset, color);
});
canvasContext.fillStyle = gradient;
} else if (options.backgroundOptions.color) {
canvasContext.fillStyle = options.backgroundOptions.color;
}
canvasContext.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
drawDots(filter?: FilterFunction): void {
if (!this._qr) {
throw "QR code is not defined";
}
const canvasContext = this.context;
if (!canvasContext) {
throw "QR code is not defined";
}
const options = this._options;
const count = this._qr.getModuleCount();
if (count > options.width || count > options.height) {
throw "The canvas is too small.";
}
const minSize = Math.min(options.width, options.height) - options.margin * 2;
const dotSize = Math.floor(minSize / count);
const xBeginning = Math.floor((options.width - count * dotSize) / 2);
const yBeginning = Math.floor((options.height - count * dotSize) / 2);
const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type });
canvasContext.beginPath();
for (let i = 0; i < count; i++) {
for (let j = 0; j < count; j++) {
if (filter && !filter(i, j)) {
continue;
}
if (!this._qr.isDark(i, j)) {
continue;
}
dot.draw(
xBeginning + i * dotSize,
yBeginning + j * dotSize,
dotSize,
(xOffset: number, yOffset: number): boolean => {
if (i + xOffset < 0 || j + yOffset < 0 || i + xOffset >= count || j + yOffset >= count) return false;
if (filter && !filter(i + xOffset, j + yOffset)) return false;
return !!this._qr && this._qr.isDark(i + xOffset, j + yOffset);
}
);
}
}
if (options.dotsOptions.gradient) {
const gradientOptions = options.dotsOptions.gradient;
const gradient = this._createGradient({
context: canvasContext,
options: gradientOptions,
additionalRotation: 0,
x: xBeginning,
y: yBeginning,
size: count * dotSize
});
gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
gradient.addColorStop(offset, color);
});
canvasContext.fillStyle = canvasContext.strokeStyle = gradient;
} else if (options.dotsOptions.color) {
canvasContext.fillStyle = canvasContext.strokeStyle = options.dotsOptions.color;
}
canvasContext.fill("evenodd");
}
drawCorners(filter?: FilterFunction): void {
if (!this._qr) {
throw "QR code is not defined";
}
const canvasContext = this.context;
if (!canvasContext) {
throw "QR code is not defined";
}
const options = this._options;
const count = this._qr.getModuleCount();
const minSize = Math.min(options.width, options.height) - options.margin * 2;
const dotSize = Math.floor(minSize / count);
const cornersSquareSize = dotSize * 7;
const cornersDotSize = dotSize * 3;
const xBeginning = Math.floor((options.width - count * dotSize) / 2);
const yBeginning = Math.floor((options.height - count * dotSize) / 2);
[
[0, 0, 0],
[1, 0, Math.PI / 2],
[0, 1, -Math.PI / 2]
].forEach(([column, row, rotation]) => {
if (filter && !filter(column, row)) {
return;
}
const x = xBeginning + column * dotSize * (count - 7);
const y = yBeginning + row * dotSize * (count - 7);
if (options.cornersSquareOptions?.type) {
const cornersSquare = new QRCornerSquare({ context: canvasContext, type: options.cornersSquareOptions?.type });
canvasContext.beginPath();
cornersSquare.draw(x, y, cornersSquareSize, rotation);
} else {
const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type });
canvasContext.beginPath();
for (let i = 0; i < squareMask.length; i++) {
for (let j = 0; j < squareMask[i].length; j++) {
if (!squareMask[i]?.[j]) {
continue;
}
dot.draw(
x + i * dotSize,
y + j * dotSize,
dotSize,
(xOffset: number, yOffset: number): boolean => !!squareMask[i + xOffset]?.[j + yOffset]
);
}
}
}
if (options.cornersSquareOptions?.gradient) {
const gradientOptions = options.cornersSquareOptions.gradient;
const gradient = this._createGradient({
context: canvasContext,
options: gradientOptions,
additionalRotation: rotation,
x,
y,
size: cornersSquareSize
});
gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
gradient.addColorStop(offset, color);
});
canvasContext.fillStyle = canvasContext.strokeStyle = gradient;
} else if (options.cornersSquareOptions?.color) {
canvasContext.fillStyle = canvasContext.strokeStyle = options.cornersSquareOptions.color;
}
canvasContext.fill("evenodd");
if (options.cornersDotOptions?.type) {
const cornersDot = new QRCornerDot({ context: canvasContext, type: options.cornersDotOptions?.type });
canvasContext.beginPath();
cornersDot.draw(x + dotSize * 2, y + dotSize * 2, cornersDotSize, rotation);
} else {
const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type });
canvasContext.beginPath();
for (let i = 0; i < dotMask.length; i++) {
for (let j = 0; j < dotMask[i].length; j++) {
if (!dotMask[i]?.[j]) {
continue;
}
dot.draw(
x + i * dotSize,
y + j * dotSize,
dotSize,
(xOffset: number, yOffset: number): boolean => !!dotMask[i + xOffset]?.[j + yOffset]
);
}
}
}
if (options.cornersDotOptions?.gradient) {
const gradientOptions = options.cornersDotOptions.gradient;
const gradient = this._createGradient({
context: canvasContext,
options: gradientOptions,
additionalRotation: rotation,
x: x + dotSize * 2,
y: y + dotSize * 2,
size: cornersDotSize
});
gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
gradient.addColorStop(offset, color);
});
canvasContext.fillStyle = canvasContext.strokeStyle = gradient;
} else if (options.cornersDotOptions?.color) {
canvasContext.fillStyle = canvasContext.strokeStyle = options.cornersDotOptions.color;
}
canvasContext.fill("evenodd");
});
}
loadImage(): Promise<void> {
return new Promise((resolve, reject) => {
const options = this._options;
const image = new Image();
if (!options.image) {
return reject("Image is not defined");
}
if (typeof options.imageOptions.crossOrigin === "string") {
image.crossOrigin = options.imageOptions.crossOrigin;
}
this._image = image;
image.onload = (): void => {
resolve();
};
image.src = options.image;
});
}
drawImage({
width,
height,
count,
dotSize
}: {
width: number;
height: number;
count: number;
dotSize: number;
}): void {
const canvasContext = this.context;
if (!canvasContext) {
throw "canvasContext is not defined";
}
if (!this._image) {
throw "image is not defined";
}
const options = this._options;
const xBeginning = Math.floor((options.width - count * dotSize) / 2);
const yBeginning = Math.floor((options.height - count * dotSize) / 2);
const dx = xBeginning + options.imageOptions.margin + (count * dotSize - width) / 2;
const dy = yBeginning + options.imageOptions.margin + (count * dotSize - height) / 2;
const dw = width - options.imageOptions.margin * 2;
const dh = height - options.imageOptions.margin * 2;
canvasContext.drawImage(this._image, dx, dy, dw < 0 ? 0 : dw, dh < 0 ? 0 : dh);
}
_createGradient({
context,
options,
additionalRotation,
x,
y,
size
}: {
context: CanvasRenderingContext2D;
options: Gradient;
additionalRotation: number;
x: number;
y: number;
size: number;
}): CanvasGradient {
let gradient;
if (options.type === gradientTypes.radial) {
gradient = context.createRadialGradient(x + size / 2, y + size / 2, 0, x + size / 2, y + size / 2, size / 2);
} else {
const rotation = ((options.rotation || 0) + additionalRotation) % (2 * Math.PI);
const positiveRotation = (rotation + 2 * Math.PI) % (2 * Math.PI);
let x0 = x + size / 2;
let y0 = y + size / 2;
let x1 = x + size / 2;
let y1 = y + size / 2;
if (
(positiveRotation >= 0 && positiveRotation <= 0.25 * Math.PI) ||
(positiveRotation > 1.75 * Math.PI && positiveRotation <= 2 * Math.PI)
) {
x0 = x0 - size / 2;
y0 = y0 - (size / 2) * Math.tan(rotation);
x1 = x1 + size / 2;
y1 = y1 + (size / 2) * Math.tan(rotation);
} else if (positiveRotation > 0.25 * Math.PI && positiveRotation <= 0.75 * Math.PI) {
y0 = y0 - size / 2;
x0 = x0 - size / 2 / Math.tan(rotation);
y1 = y1 + size / 2;
x1 = x1 + size / 2 / Math.tan(rotation);
} else if (positiveRotation > 0.75 * Math.PI && positiveRotation <= 1.25 * Math.PI) {
x0 = x0 + size / 2;
y0 = y0 + (size / 2) * Math.tan(rotation);
x1 = x1 - size / 2;
y1 = y1 - (size / 2) * Math.tan(rotation);
} else if (positiveRotation > 1.25 * Math.PI && positiveRotation <= 1.75 * Math.PI) {
y0 = y0 + size / 2;
x0 = x0 + size / 2 / Math.tan(rotation);
y1 = y1 - size / 2;
x1 = x1 - size / 2 / Math.tan(rotation);
}
gradient = context.createLinearGradient(Math.round(x0), Math.round(y0), Math.round(x1), Math.round(y1));
}
return gradient;
}
}