import FontFaceObserver from 'fontfaceobserver' import { CanvaOptions, CanvasSettings, CanvasSubTypes, Element, ImageItem, Style, TextItem } from './type' export { CanvasSubTypes } from './type' class Canvas { private canvas: HTMLCanvasElement private ctx: CanvasRenderingContext2D private settings: CanvasSettings private options: CanvaOptions private imageList: any = {} private imageLoader: Promise private loader: Promise private ratio = 1 public constructor( settings: CanvasSettings, options: CanvaOptions, element?: Element ) { const { width, height, bgColor, bgImage, usePrimaryFont, ratio } = settings if (ratio) this.ratio = ratio this.canvas = (element as HTMLCanvasElement) || document.createElement('canvas') this.canvas.width = width * this.ratio this.canvas.height = height * this.ratio this.canvas.setAttribute( 'style', '-moz-osx-font-smoothing: grayscale;-webkit-font-smoothing: antialiased;display:none' ) document.body.appendChild(this.canvas) this.ctx = this.canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D this.settings = settings this.options = options if (bgColor) { this.drawRect(0, 0, width, height, { fillStyle: bgColor }) } this.imageLoader = Promise.resolve() this.collectImage() let fontLoader = Promise.resolve() if (usePrimaryFont) { this.canvas.style.fontFamily = 'FZY4JW' const FONT_LOAD_MAX_TIME = 30000 const FONT_NAME = 'FZY4JW' const font = new FontFaceObserver(FONT_NAME) fontLoader = font.load(null, FONT_LOAD_MAX_TIME) } this.loader = Promise.all([this.imageLoader, fontLoader]) this.loader.then(() => { if (bgImage) { this.drawImage({ type: CanvasSubTypes.Image, src: bgImage, position: { x: 0, y: 0, width, height } }) } this.initOptions() }) } public getExtra(extra: any) { const result = { ...extra } if (result.font) { let fontSize = result.font.match(/\d+px/) fontSize = parseInt(fontSize, 10) * this.ratio result.font = result.font.replace(/\d+px/, `${fontSize}px`) } return result } public initOptions() { this.options.forEach(option => { this.ctx.beginPath() Object.assign(this.ctx, this.getExtra(option.extra)) switch (option.type) { case CanvasSubTypes.Text: this.createText(option) break case CanvasSubTypes.Image: this.drawImage(option) break } }) } public createText(item: TextItem) { const { position, text, maxWidth, lineHeight, styled, extra, del } = item const isDelete = Boolean(del) let { x, y } = position this.ctx.lineWidth = 1 if (maxWidth && lineHeight) { const textArr = text.split('') let line = '' textArr.forEach((word, index) => { const testLine = line + word const metrics = this.ctx.measureText(testLine) const testWidth = metrics.width if (testWidth > maxWidth * this.ratio && index) { this.drawText(line, x, y, isDelete) line = word y += lineHeight } else { line = testLine } }) this.drawText(line, x, y, isDelete) } else if (styled) { const textArr = text.split('') let line = '' textArr.forEach(word => { if (word === '<') { this.drawText(line, x, y, isDelete) x += this.ctx.measureText(line).width / this.ratio Object.assign(this.ctx, styled) line = '' } else if (word === '>') { this.drawText(line, x, y, isDelete) x += this.ctx.measureText(line).width / this.ratio Object.assign(this.ctx, this.getExtra(extra)) line = '' } else { line += word } }) this.drawText(line, x, y, isDelete) } else { this.drawText(text, x, y, isDelete) } } public collectImage() { const { settings, options } = this const { bgImage } = settings if (bgImage) this.imageList[bgImage] = null options.forEach(option => { if (option.type === CanvasSubTypes.Image) { this.imageList[option.src] = null } }) this.imageLoader = Promise.all( Object.keys(this.imageList).map(imgSrc => { return new Promise((resolve, reject) => { const image = new Image() if (!/^data:(.*);base64,/.test(imgSrc)) { image.setAttribute('crossOrigin', 'anonymous') } image.onload = () => { this.imageList[imgSrc] = image resolve(image) } image.onerror = () => { reject(image) } image.src = imgSrc }) }) ) } public drawImage(item: ImageItem) { const { src, position } = item const { x, y, width = this.imageList[src].width, height = this.imageList[src].height } = position this.ctx.drawImage( this.imageList[src], x * this.ratio, y * this.ratio, width * this.ratio, height * this.ratio ) } public drawText(text: string, x: number, y: number, del: boolean) { this.ctx.fillText(text, x * this.ratio, y * this.ratio) if (del) { const { width } = this.ctx.measureText(text) const fontSizeMath = this.ctx.font.match(/\d+/) const fontSize = parseFloat((fontSizeMath && fontSizeMath[0]) || '0') const verticalPos = this.ratio * y - fontSize / 2 + 1 this.ctx.moveTo(x * this.ratio, verticalPos) this.ctx.lineWidth = 2 * this.ratio const { fillStyle } = this.ctx this.ctx.strokeStyle = fillStyle this.ctx.lineTo(this.ratio * x + width, verticalPos) this.ctx.stroke() } } public drawRect( x: number, y: number, width: number, height: number, options: Style ) { const { fillStyle, strokeStyle } = options this.ctx.beginPath() this.ctx.rect( x * this.ratio, y * this.ratio, width * this.ratio, height * this.ratio ) if (fillStyle) { this.ctx.fillStyle = fillStyle this.ctx.fill() } if (strokeStyle) { this.ctx.strokeStyle = strokeStyle this.ctx.stroke() } } public getBase64Data() { return this.loader.then(() => { return this.canvas.toDataURL('image/png', 1.0) }) } } /** 获取图片base64信息 */ export default function getImage( settings: CanvasSettings, options: CanvaOptions, element?: Element ) { const image = new Canvas(settings, options, element) return image.getBase64Data() }