From eadf7f6e49df4534a49f3e7c263c2caca96b3c3a Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Wed, 10 Jan 2024 21:25:27 +0800 Subject: [PATCH] feat: add background image option --- docs/en/guide/option.md | 2 +- docs/guide/option.md | 2 +- src/editor/core/draw/frame/Background.ts | 99 +++++++++++++++++++++-- src/editor/dataset/constant/Background.ts | 9 +++ src/editor/dataset/enum/Background.ts | 11 +++ src/editor/index.ts | 15 +++- src/editor/interface/Background.ts | 8 ++ src/editor/interface/Editor.ts | 3 +- 8 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 src/editor/dataset/constant/Background.ts create mode 100644 src/editor/dataset/enum/Background.ts create mode 100644 src/editor/interface/Background.ts diff --git a/docs/en/guide/option.md b/docs/en/guide/option.md index 847db93..434c85b 100644 --- a/docs/en/guide/option.md +++ b/docs/en/guide/option.md @@ -27,7 +27,6 @@ interface IEditorOption { height?: number // Paper height. default: 1123 scale?: number // scaling. default: 1 pageGap?: number // Paper spacing. default: 20 - backgroundColor?: string // Paper background color. default: #FFFFFF underlineColor?: string // Underline color. default: #000000 strikeoutColor?: string // Strikeout color. default: #FF0000 rangeColor?: string // Range color. default: #AECBFA @@ -68,6 +67,7 @@ interface IEditorOption { group?: IGroup // Group option. {opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean} pageBreak?: IPageBreak // PageBreak option。{font?:string; fontSize?:number; lineDash?:number[];} zone?: IZoneOption // Zone option。{tipDisabled?:boolean;} + background?: IBackgroundOption // Background option. {color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat;}。default: {color: '#FFFFFF'} } ``` diff --git a/docs/guide/option.md b/docs/guide/option.md index 6aeb3a7..0d0c858 100644 --- a/docs/guide/option.md +++ b/docs/guide/option.md @@ -27,7 +27,6 @@ interface IEditorOption { height?: number // 纸张高度。默认:1123 scale?: number // 缩放比例。默认:1 pageGap?: number // 纸张间隔。默认:20 - backgroundColor?: string // 纸张背景色。默认:#FFFFFF underlineColor?: string // 下划线颜色。默认:#000000 strikeoutColor?: string // 删除线颜色。默认:#FF0000 rangeColor?: string // 选区颜色。默认:#AECBFA @@ -68,6 +67,7 @@ interface IEditorOption { group?: IGroup // 成组配置。{opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean} pageBreak?: IPageBreak // 分页符配置。{font?:string; fontSize?:number; lineDash?:number[];} zone?: IZoneOption // 编辑器区域配置。{tipDisabled?:boolean;} + background?: IBackgroundOption // 背景配置。{color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat;}。默认:{color: '#FFFFFF'} } ``` diff --git a/src/editor/core/draw/frame/Background.ts b/src/editor/core/draw/frame/Background.ts index c1c45f4..232e307 100644 --- a/src/editor/core/draw/frame/Background.ts +++ b/src/editor/core/draw/frame/Background.ts @@ -1,3 +1,7 @@ +import { + BackgroundRepeat, + BackgroundSize +} from '../../../dataset/enum/Background' import { DeepRequired } from '../../../interface/Common' import { IEditorOption } from '../../../interface/Editor' import { Draw } from '../Draw' @@ -5,19 +9,104 @@ import { Draw } from '../Draw' export class Background { private draw: Draw private options: DeepRequired + private imageCache: Map constructor(draw: Draw) { this.draw = draw this.options = draw.getOptions() + this.imageCache = new Map() } - public render(ctx: CanvasRenderingContext2D, pageNo: number) { - const { backgroundColor } = this.options - const width = this.draw.getCanvasWidth(pageNo) - const height = this.draw.getCanvasHeight(pageNo) + private _renderBackgroundColor( + ctx: CanvasRenderingContext2D, + color: string, + width: number, + height: number + ) { ctx.save() - ctx.fillStyle = backgroundColor + ctx.fillStyle = color ctx.fillRect(0, 0, width, height) ctx.restore() } + + private _drawImage( + ctx: CanvasRenderingContext2D, + imageElement: HTMLImageElement, + width: number, + height: number + ) { + const { background, scale } = this.options + // contain + if (background.size === BackgroundSize.CONTAIN) { + const imageWidth = imageElement.width * scale + const imageHeight = imageElement.height * scale + if ( + !background.repeat || + background.repeat === BackgroundRepeat.NO_REPEAT + ) { + ctx.drawImage(imageElement, 0, 0, imageWidth, imageHeight) + } else { + let startX = 0 + let startY = 0 + const repeatXCount = + background.repeat === BackgroundRepeat.REPEAT || + background.repeat === BackgroundRepeat.REPEAT_X + ? Math.ceil((width * scale) / imageWidth) + : 1 + const repeatYCount = + background.repeat === BackgroundRepeat.REPEAT || + background.repeat === BackgroundRepeat.REPEAT_Y + ? Math.ceil((height * scale) / imageHeight) + : 1 + for (let x = 0; x < repeatXCount; x++) { + for (let y = 0; y < repeatYCount; y++) { + ctx.drawImage(imageElement, startX, startY, imageWidth, imageHeight) + startY += imageHeight + } + startY = 0 + startX += imageWidth + } + } + } else { + // cover + ctx.drawImage(imageElement, 0, 0, width * scale, height * scale) + } + } + + private _renderBackgroundImage( + ctx: CanvasRenderingContext2D, + width: number, + height: number + ) { + const { background } = this.options + const imageElementCache = this.imageCache.get(background.image) + if (imageElementCache) { + this._drawImage(ctx, imageElementCache, width, height) + } else { + const img = new Image() + img.setAttribute('crossOrigin', 'Anonymous') + img.src = background.image + img.onload = () => { + this.imageCache.set(background.image, img) + this._drawImage(ctx, img, width, height) + // 避免层级上浮,触发编辑器二次渲染 + this.draw.render({ + isCompute: false, + isSubmitHistory: false + }) + } + } + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + const { background } = this.options + if (background.image) { + const { width, height } = this.options + this._renderBackgroundImage(ctx, width, height) + } else { + const width = this.draw.getCanvasWidth(pageNo) + const height = this.draw.getCanvasHeight(pageNo) + this._renderBackgroundColor(ctx, background.color, width, height) + } + } } diff --git a/src/editor/dataset/constant/Background.ts b/src/editor/dataset/constant/Background.ts new file mode 100644 index 0000000..8088928 --- /dev/null +++ b/src/editor/dataset/constant/Background.ts @@ -0,0 +1,9 @@ +import { IBackgroundOption } from '../../interface/Background' +import { BackgroundRepeat, BackgroundSize } from '../enum/Background' + +export const defaultBackground: Readonly> = { + color: '#FFFFFF', + image: '', + size: BackgroundSize.COVER, + repeat: BackgroundRepeat.NO_REPEAT +} diff --git a/src/editor/dataset/enum/Background.ts b/src/editor/dataset/enum/Background.ts new file mode 100644 index 0000000..9ec0f41 --- /dev/null +++ b/src/editor/dataset/enum/Background.ts @@ -0,0 +1,11 @@ +export enum BackgroundSize { + CONTAIN = 'contain', + COVER = 'cover' +} + +export enum BackgroundRepeat { + REPEAT = 'repeat', + NO_REPEAT = 'no-repeat', + REPEAT_X = 'repeat-x', + REPEAT_Y = 'repeat-y' +} diff --git a/src/editor/index.ts b/src/editor/index.ts index 6572ebb..a79aadb 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -75,6 +75,9 @@ import { IRange } from './interface/Range' import { deepClone, splitText } from './utils' import { IZoneOption } from './interface/Zone' import { defaultZoneOption } from './dataset/constant/Zone' +import { IBackgroundOption } from './interface/Background' +import { defaultBackground } from './dataset/constant/Background' +import { BackgroundRepeat, BackgroundSize } from './dataset/enum/Background' export default class Editor { public command: Command @@ -138,6 +141,10 @@ export default class Editor { ...defaultZoneOption, ...options.zone } + const backgroundOptions: Required = { + ...defaultBackground, + ...options.background + } const editorOptions: DeepRequired = { mode: EditorMode.EDIT, @@ -153,7 +160,6 @@ export default class Editor { height: 1123, scale: 1, pageGap: 20, - backgroundColor: '#FFFFFF', underlineColor: '#000000', strikeoutColor: '#FF0000', rangeAlpha: 0.6, @@ -194,7 +200,8 @@ export default class Editor { placeholder: placeholderOptions, group: groupOptions, pageBreak: pageBreakOptions, - zone: zoneOptions + zone: zoneOptions, + background: backgroundOptions } // 数据处理 data = deepClone(data) @@ -292,7 +299,9 @@ export { ListType, ListStyle, WordBreak, - ControlIndentation + ControlIndentation, + BackgroundRepeat, + BackgroundSize } // 对外类型 diff --git a/src/editor/interface/Background.ts b/src/editor/interface/Background.ts new file mode 100644 index 0000000..46f1f5f --- /dev/null +++ b/src/editor/interface/Background.ts @@ -0,0 +1,8 @@ +import { BackgroundRepeat, BackgroundSize } from '../dataset/enum/Background' + +export interface IBackgroundOption { + color?: string + image?: string + size?: BackgroundSize + repeat?: BackgroundRepeat +} diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts index 8e547ae..7b92b3e 100644 --- a/src/editor/interface/Editor.ts +++ b/src/editor/interface/Editor.ts @@ -5,6 +5,7 @@ import { PaperDirection, WordBreak } from '../dataset/enum/Editor' +import { IBackgroundOption } from './Background' import { ICheckboxOption } from './Checkbox' import { IPadding } from './Common' import { IControlOption } from './Control' @@ -40,7 +41,6 @@ export interface IEditorOption { height?: number scale?: number pageGap?: number - backgroundColor?: string underlineColor?: string strikeoutColor?: string rangeColor?: string @@ -81,6 +81,7 @@ export interface IEditorOption { group?: IGroup pageBreak?: IPageBreak zone?: IZoneOption + background?: IBackgroundOption } export interface IEditorResult {