commit
51b275c8a9
@ -0,0 +1,37 @@
|
||||
import Editor from '../../../src/editor'
|
||||
|
||||
describe('菜单-LaTeX', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('http://localhost:3000/canvas-editor/')
|
||||
|
||||
cy.get('canvas').first().as('canvas').should('have.length', 1)
|
||||
})
|
||||
|
||||
const text = 'canvas-editor'
|
||||
|
||||
it('LaTeX', () => {
|
||||
cy.getEditor().then((editor: Editor) => {
|
||||
editor.listener.saved = function (payload) {
|
||||
const data = payload.data
|
||||
|
||||
expect(data[0].type).to.eq('latex')
|
||||
|
||||
expect(data[0].value).to.eq(text)
|
||||
}
|
||||
|
||||
editor.command.executeSelectAll()
|
||||
|
||||
editor.command.executeBackspace()
|
||||
|
||||
cy.get('.menu-item__latex').click()
|
||||
|
||||
cy.get('.dialog-option__item [name="value"]').type(text)
|
||||
|
||||
cy.get('.dialog-menu button').eq(1).click()
|
||||
|
||||
cy.get('@canvas').type('{ctrl}s')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
After Width: | Height: | Size: 393 B |
@ -0,0 +1,34 @@
|
||||
import { IElement } from '../../../../interface/Element'
|
||||
import { ImageParticle } from '../ImageParticle'
|
||||
import { LaTexSVG, LaTexUtils } from './utils/LaTeXUtils'
|
||||
|
||||
export class LaTexParticle extends ImageParticle {
|
||||
|
||||
public static convertLaTextToSVG(laTex: string): LaTexSVG {
|
||||
return new LaTexUtils(laTex).svg({
|
||||
SCALE_X: 10,
|
||||
SCALE_Y: 10,
|
||||
MARGIN_X: 0,
|
||||
MARGIN_Y: 0
|
||||
})
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, element: IElement, x: number, y: number) {
|
||||
const { scale } = this.options
|
||||
const width = element.width! * scale
|
||||
const height = element.height! * scale
|
||||
if (this.imageCache.has(element.value)) {
|
||||
const img = this.imageCache.get(element.value)!
|
||||
ctx.drawImage(img, x, y, width, height)
|
||||
} else {
|
||||
const img = new Image()
|
||||
img.src = element.laTexSVG!
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, x, y, width, height)
|
||||
this.imageCache.set(element.value, img)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,298 @@
|
||||
/*
|
||||
https://oeis.org/wiki/List_of_LaTeX_mathematical_symbols
|
||||
https://en.wikibooks.org/wiki/LaTeX/Mathematics
|
||||
*/
|
||||
export interface Symb {
|
||||
glyph: number;
|
||||
arity?: number;
|
||||
flags: Record<string, boolean>;
|
||||
}
|
||||
|
||||
const SYMB: Record<string, Symb> = {
|
||||
'\\frac': { glyph: 0, arity: 2, flags: {} },
|
||||
'\\binom': { glyph: 0, arity: 2, flags: {} },
|
||||
'\\sqrt': { glyph: 2267, arity: 1, flags: { opt: true, xfl: true, yfl: true } },
|
||||
'^': { glyph: 0, arity: 1, flags: {} },
|
||||
'_': { glyph: 0, arity: 1, flags: {} },
|
||||
'(': { glyph: 2221, arity: 0, flags: { yfl: true } },
|
||||
')': { glyph: 2222, arity: 0, flags: { yfl: true } },
|
||||
'[': { glyph: 2223, arity: 0, flags: { yfl: true } },
|
||||
']': { glyph: 2224, arity: 0, flags: { yfl: true } },
|
||||
'\\langle': { glyph: 2227, arity: 0, flags: { yfl: true } },
|
||||
'\\rangle': { glyph: 2228, arity: 0, flags: { yfl: true } },
|
||||
'|': { glyph: 2229, arity: 0, flags: { yfl: true } },
|
||||
|
||||
'\\|': { glyph: 2230, arity: 0, flags: { yfl: true } },
|
||||
'\\{': { glyph: 2225, arity: 0, flags: { yfl: true } },
|
||||
'\\}': { glyph: 2226, arity: 0, flags: { yfl: true } },
|
||||
|
||||
'\\#': { glyph: 2275, arity: 0, flags: {} },
|
||||
'\\$': { glyph: 2274, arity: 0, flags: {} },
|
||||
'\\&': { glyph: 2273, arity: 0, flags: {} },
|
||||
'\\%': { glyph: 2271, arity: 0, flags: {} },
|
||||
|
||||
/*semantics*/
|
||||
'\\begin': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\end': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\left': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\right': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\middle': { glyph: 0, arity: 1, flags: {} },
|
||||
|
||||
/*operators*/
|
||||
'\\cdot': { glyph: 2236, arity: 0, flags: {} },
|
||||
'\\pm': { glyph: 2233, arity: 0, flags: {} },
|
||||
'\\mp': { glyph: 2234, arity: 0, flags: {} },
|
||||
'\\times': { glyph: 2235, arity: 0, flags: {} },
|
||||
'\\div': { glyph: 2237, arity: 0, flags: {} },
|
||||
'\\leqq': { glyph: 2243, arity: 0, flags: {} },
|
||||
'\\geqq': { glyph: 2244, arity: 0, flags: {} },
|
||||
'\\leq': { glyph: 2243, arity: 0, flags: {} },
|
||||
'\\geq': { glyph: 2244, arity: 0, flags: {} },
|
||||
'\\propto': { glyph: 2245, arity: 0, flags: {} },
|
||||
'\\sim': { glyph: 2246, arity: 0, flags: {} },
|
||||
'\\equiv': { glyph: 2240, arity: 0, flags: {} },
|
||||
'\\dagger': { glyph: 2277, arity: 0, flags: {} },
|
||||
'\\ddagger': { glyph: 2278, arity: 0, flags: {} },
|
||||
'\\ell': { glyph: 662, arity: 0, flags: {} },
|
||||
|
||||
/*accents*/
|
||||
'\\vec': { glyph: 2261, arity: 1, flags: { hat: true, xfl: true, yfl: true } },
|
||||
'\\overrightarrow': { glyph: 2261, arity: 1, flags: { hat: true, xfl: true, yfl: true } },
|
||||
'\\overleftarrow': { glyph: 2263, arity: 1, flags: { hat: true, xfl: true, yfl: true } },
|
||||
'\\bar': { glyph: 2231, arity: 1, flags: { hat: true, xfl: true } },
|
||||
'\\overline': { glyph: 2231, arity: 1, flags: { hat: true, xfl: true } },
|
||||
'\\widehat': { glyph: 2247, arity: 1, flags: { hat: true, xfl: true, yfl: true } },
|
||||
'\\hat': { glyph: 2247, arity: 1, flags: { hat: true } },
|
||||
'\\acute': { glyph: 2248, arity: 1, flags: { hat: true } },
|
||||
'\\grave': { glyph: 2249, arity: 1, flags: { hat: true } },
|
||||
'\\breve': { glyph: 2250, arity: 1, flags: { hat: true } },
|
||||
'\\tilde': { glyph: 2246, arity: 1, flags: { hat: true } },
|
||||
'\\underline': { glyph: 2231, arity: 1, flags: { mat: true, xfl: true } },
|
||||
|
||||
'\\not': { glyph: 2220, arity: 1, flags: {} },
|
||||
|
||||
'\\neq': { glyph: 2239, arity: 1, flags: {} },
|
||||
'\\ne': { glyph: 2239, arity: 1, flags: {} },
|
||||
'\\exists': { glyph: 2279, arity: 0, flags: {} },
|
||||
'\\in': { glyph: 2260, arity: 0, flags: {} },
|
||||
'\\subset': { glyph: 2256, arity: 0, flags: {} },
|
||||
'\\supset': { glyph: 2258, arity: 0, flags: {} },
|
||||
'\\cup': { glyph: 2257, arity: 0, flags: {} },
|
||||
'\\cap': { glyph: 2259, arity: 0, flags: {} },
|
||||
'\\infty': { glyph: 2270, arity: 0, flags: {} },
|
||||
'\\partial': { glyph: 2265, arity: 0, flags: {} },
|
||||
'\\nabla': { glyph: 2266, arity: 0, flags: {} },
|
||||
'\\aleph': { glyph: 2077, arity: 0, flags: {} },
|
||||
'\\wp': { glyph: 2190, arity: 0, flags: {} },
|
||||
'\\therefore': { glyph: 740, arity: 0, flags: {} },
|
||||
'\\mid': { glyph: 2229, arity: 0, flags: {} },
|
||||
|
||||
'\\sum': { glyph: 2402, arity: 0, flags: { big: true } },
|
||||
'\\prod': { glyph: 2401, arity: 0, flags: { big: true } },
|
||||
'\\bigoplus': { glyph: 2284, arity: 0, flags: { big: true } },
|
||||
'\\bigodot': { glyph: 2281, arity: 0, flags: { big: true } },
|
||||
'\\int': { glyph: 2412, arity: 0, flags: { yfl: true } },
|
||||
'\\oint': { glyph: 2269, arity: 0, flags: { yfl: true } },
|
||||
'\\oplus': { glyph: 1284, arity: 0, flags: {} },
|
||||
'\\odot': { glyph: 1281, arity: 0, flags: {} },
|
||||
'\\perp': { glyph: 738, arity: 0, flags: {} },
|
||||
'\\angle': { glyph: 739, arity: 0, flags: {} },
|
||||
'\\triangle': { glyph: 842, arity: 0, flags: {} },
|
||||
'\\Box': { glyph: 841, arity: 0, flags: {} },
|
||||
|
||||
'\\rightarrow': { glyph: 2261, arity: 0, flags: {} },
|
||||
'\\to': { glyph: 2261, arity: 0, flags: {} },
|
||||
'\\leftarrow': { glyph: 2263, arity: 0, flags: {} },
|
||||
'\\gets': { glyph: 2263, arity: 0, flags: {} },
|
||||
'\\circ': { glyph: 902, arity: 0, flags: {} },
|
||||
'\\bigcirc': { glyph: 904, arity: 0, flags: {} },
|
||||
'\\bullet': { glyph: 828, arity: 0, flags: {} },
|
||||
'\\star': { glyph: 856, arity: 0, flags: {} },
|
||||
'\\diamond': { glyph: 743, arity: 0, flags: {} },
|
||||
'\\ast': { glyph: 728, arity: 0, flags: {} },
|
||||
|
||||
/*verbatim symbols*/
|
||||
'\\log': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\ln': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\exp': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\mod': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\lim': { glyph: 0, arity: 0, flags: { txt: true, big: true } },
|
||||
|
||||
'\\sin': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\cos': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\tan': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\csc': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\sec': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\cot': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\sinh': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\cosh': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\tanh': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\csch': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\sech': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\coth': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\arcsin': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\arccos': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\arctan': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\arccsc': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\arcsec': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
'\\arccot': { glyph: 0, arity: 0, flags: { txt: true } },
|
||||
|
||||
/*font modes*/
|
||||
'\\text': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathnormal': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathrm': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathit': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathbf': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathsf': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathtt': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathfrak': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathcal': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathbb': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\mathscr': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\rm': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\it': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\bf': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\sf': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\tt': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\frak': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\cal': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\bb': { glyph: 0, arity: 1, flags: {} },
|
||||
'\\scr': { glyph: 0, arity: 1, flags: {} },
|
||||
|
||||
'\\quad': { glyph: 0, arity: 0, flags: {} },
|
||||
'\\,': { glyph: 0, arity: 0, flags: {} },
|
||||
'\\.': { glyph: 0, arity: 0, flags: {} },
|
||||
'\\;': { glyph: 0, arity: 0, flags: {} },
|
||||
'\\!': { glyph: 0, arity: 0, flags: {} },
|
||||
|
||||
/*greek letters*/
|
||||
'\\alpha': { glyph: 2127, flags: {} },
|
||||
'\\beta': { glyph: 2128, flags: {} },
|
||||
'\\gamma': { glyph: 2129, flags: {} },
|
||||
'\\delta': { glyph: 2130, flags: {} },
|
||||
'\\varepsilon': { glyph: 2131, flags: {} },
|
||||
'\\zeta': { glyph: 2132, flags: {} },
|
||||
'\\eta': { glyph: 2133, flags: {} },
|
||||
'\\vartheta': { glyph: 2134, flags: {} },
|
||||
'\\iota': { glyph: 2135, flags: {} },
|
||||
'\\kappa': { glyph: 2136, flags: {} },
|
||||
'\\lambda': { glyph: 2137, flags: {} },
|
||||
'\\mu': { glyph: 2138, flags: {} },
|
||||
'\\nu': { glyph: 2139, flags: {} },
|
||||
'\\xi': { glyph: 2140, flags: {} },
|
||||
'\\omicron': { glyph: 2141, flags: {} },
|
||||
'\\pi': { glyph: 2142, flags: {} },
|
||||
'\\rho': { glyph: 2143, flags: {} },
|
||||
'\\sigma': { glyph: 2144, flags: {} },
|
||||
'\\tau': { glyph: 2145, flags: {} },
|
||||
'\\upsilon': { glyph: 2146, flags: {} },
|
||||
'\\varphi': { glyph: 2147, flags: {} },
|
||||
'\\chi': { glyph: 2148, flags: {} },
|
||||
'\\psi': { glyph: 2149, flags: {} },
|
||||
'\\omega': { glyph: 2150, flags: {} },
|
||||
|
||||
'\\epsilon': { glyph: 2184, flags: {} },
|
||||
'\\theta': { glyph: 2185, flags: {} },
|
||||
'\\phi': { glyph: 2186, flags: {} },
|
||||
'\\varsigma': { glyph: 2187, flags: {} },
|
||||
|
||||
'\\Alpha': { glyph: 2027, flags: {} },
|
||||
'\\Beta': { glyph: 2028, flags: {} },
|
||||
'\\Gamma': { glyph: 2029, flags: {} },
|
||||
'\\Delta': { glyph: 2030, flags: {} },
|
||||
'\\Epsilon': { glyph: 2031, flags: {} },
|
||||
'\\Zeta': { glyph: 2032, flags: {} },
|
||||
'\\Eta': { glyph: 2033, flags: {} },
|
||||
'\\Theta': { glyph: 2034, flags: {} },
|
||||
'\\Iota': { glyph: 2035, flags: {} },
|
||||
'\\Kappa': { glyph: 2036, flags: {} },
|
||||
'\\Lambda': { glyph: 2037, flags: {} },
|
||||
'\\Mu': { glyph: 2038, flags: {} },
|
||||
'\\Nu': { glyph: 2039, flags: {} },
|
||||
'\\Xi': { glyph: 2040, flags: {} },
|
||||
'\\Omicron': { glyph: 2041, flags: {} },
|
||||
'\\Pi': { glyph: 2042, flags: {} },
|
||||
'\\Rho': { glyph: 2043, flags: {} },
|
||||
'\\Sigma': { glyph: 2044, flags: {} },
|
||||
'\\Tau': { glyph: 2045, flags: {} },
|
||||
'\\Upsilon': { glyph: 2046, flags: {} },
|
||||
'\\Phi': { glyph: 2047, flags: {} },
|
||||
'\\Chi': { glyph: 2048, flags: {} },
|
||||
'\\Psi': { glyph: 2049, flags: {} },
|
||||
'\\Omega': { glyph: 2050, flags: {} },
|
||||
}
|
||||
|
||||
export { SYMB }
|
||||
|
||||
export function asciiMap(x: string, mode = 'math'): number {
|
||||
const c = x.charCodeAt(0)
|
||||
if (65 <= c && c <= 90) {
|
||||
const d = c - 65
|
||||
if (mode == 'text' || mode == 'rm') {
|
||||
return d + 2001
|
||||
} else if (mode == 'tt') {
|
||||
return d + 501
|
||||
} else if (mode == 'bf' || mode == 'bb') {
|
||||
return d + 3001
|
||||
} else if (mode == 'sf') {
|
||||
return d + 2501
|
||||
} else if (mode == 'frak') {
|
||||
return d + 3301
|
||||
} else if (mode == 'scr' || mode == 'cal') {
|
||||
return d + 2551
|
||||
} else {
|
||||
return d + 2051
|
||||
}
|
||||
}
|
||||
if (97 <= c && c <= 122) {
|
||||
const d = c - 97
|
||||
if (mode == 'text' || mode == 'rm') {
|
||||
return d + 2101
|
||||
} else if (mode == 'tt') {
|
||||
return d + 601
|
||||
} else if (mode == 'bf' || mode == 'bb') {
|
||||
return d + 3101
|
||||
} else if (mode == 'sf') {
|
||||
return d + 2601
|
||||
} else if (mode == 'frak') {
|
||||
return d + 3401
|
||||
} else if (mode == 'scr' || mode == 'cal') {
|
||||
return d + 2651
|
||||
} else {
|
||||
return d + 2151
|
||||
}
|
||||
}
|
||||
if (48 <= c && c <= 57) {
|
||||
const d = c - 48
|
||||
if (mode == 'it') {
|
||||
return d + 2750
|
||||
} else if (mode == 'bf') {
|
||||
return d + 3200
|
||||
} else if (mode == 'tt') {
|
||||
return d + 700
|
||||
} else {
|
||||
return d + 2200
|
||||
}
|
||||
}
|
||||
|
||||
return <number>{
|
||||
'.': 2210,
|
||||
',': 2211,
|
||||
':': 2212,
|
||||
';': 2213,
|
||||
'!': 2214,
|
||||
'?': 2215,
|
||||
'\'': 2216,
|
||||
'"': 2217,
|
||||
'*': 2219,
|
||||
'/': 2220,
|
||||
'-': 2231,
|
||||
'+': 2232,
|
||||
'=': 2238,
|
||||
'<': 2241,
|
||||
'>': 2242,
|
||||
'~': 2246,
|
||||
'@': 2273,
|
||||
'\\': 804,
|
||||
}[x]
|
||||
}
|
||||
@ -0,0 +1,335 @@
|
||||
import { IEditorOption } from '../../../../interface/Editor'
|
||||
import { IElement, IElementPosition } from '../../../../interface/Element'
|
||||
import { IPreviewerCreateResult, IPreviewerDrawOption } from '../../../../interface/Previewer'
|
||||
import { downloadFile } from '../../../../utils'
|
||||
import { Draw } from '../../Draw'
|
||||
|
||||
export class Previewer {
|
||||
|
||||
private container: HTMLDivElement
|
||||
private canvas: HTMLCanvasElement
|
||||
private draw: Draw
|
||||
private options: Required<IEditorOption>
|
||||
private curElement: IElement | null
|
||||
private curElementSrc: string
|
||||
private previewerDrawOption: IPreviewerDrawOption
|
||||
private curPosition: IElementPosition | null
|
||||
// 拖拽改变尺寸
|
||||
private resizerSelection: HTMLDivElement
|
||||
private resizerHandleList: HTMLDivElement[]
|
||||
private resizerImageContainer: HTMLDivElement
|
||||
private resizerImage: HTMLImageElement
|
||||
private width: number
|
||||
private height: number
|
||||
private mousedownX: number
|
||||
private mousedownY: number
|
||||
private curHandleIndex: number
|
||||
// 预览选区
|
||||
private previewerContainer: HTMLDivElement | null
|
||||
private previewerImage: HTMLImageElement | null
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.container = draw.getContainer()
|
||||
this.canvas = draw.getPage()
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.curElement = null
|
||||
this.curElementSrc = ''
|
||||
this.previewerDrawOption = {}
|
||||
this.curPosition = null
|
||||
// 图片尺寸缩放
|
||||
const { resizerSelection, resizerHandleList, resizerImageContainer, resizerImage } = this._createResizerDom()
|
||||
this.resizerSelection = resizerSelection
|
||||
this.resizerHandleList = resizerHandleList
|
||||
this.resizerImageContainer = resizerImageContainer
|
||||
this.resizerImage = resizerImage
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.mousedownX = 0
|
||||
this.mousedownY = 0
|
||||
this.curHandleIndex = 0 // 默认右下角
|
||||
// 图片预览
|
||||
resizerSelection.ondblclick = this._dblclick.bind(this)
|
||||
this.previewerContainer = null
|
||||
this.previewerImage = null
|
||||
}
|
||||
|
||||
private _createResizerDom(): IPreviewerCreateResult {
|
||||
// 拖拽边框
|
||||
const resizerSelection = document.createElement('div')
|
||||
resizerSelection.classList.add('resizer-selection')
|
||||
resizerSelection.style.display = 'none'
|
||||
resizerSelection.style.borderColor = this.options.resizerColor
|
||||
const resizerHandleList: HTMLDivElement[] = []
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const handleDom = document.createElement('div')
|
||||
handleDom.style.background = this.options.resizerColor
|
||||
handleDom.classList.add(`handle-${i}`)
|
||||
handleDom.setAttribute('data-index', String(i))
|
||||
handleDom.onmousedown = this._mousedown.bind(this)
|
||||
resizerSelection.append(handleDom)
|
||||
resizerHandleList.push(handleDom)
|
||||
}
|
||||
this.container.append(resizerSelection)
|
||||
// 拖拽镜像
|
||||
const resizerImageContainer = document.createElement('div')
|
||||
resizerImageContainer.classList.add('resizer-image')
|
||||
resizerImageContainer.style.display = 'none'
|
||||
const resizerImage = document.createElement('img')
|
||||
resizerImageContainer.append(resizerImage)
|
||||
this.container.append(resizerImageContainer)
|
||||
return { resizerSelection, resizerHandleList, resizerImageContainer, resizerImage }
|
||||
}
|
||||
|
||||
private _mousedown(evt: MouseEvent) {
|
||||
this.canvas = this.draw.getPage()
|
||||
if (!this.curPosition || !this.curElement) return
|
||||
const { scale } = this.options
|
||||
const height = this.draw.getHeight()
|
||||
const pageGap = this.draw.getPageGap()
|
||||
this.mousedownX = evt.x
|
||||
this.mousedownY = evt.y
|
||||
const target = evt.target as HTMLDivElement
|
||||
this.curHandleIndex = Number(target.dataset.index)
|
||||
// 改变光标
|
||||
const cursor = window.getComputedStyle(target).cursor
|
||||
document.body.style.cursor = cursor
|
||||
this.canvas.style.cursor = cursor
|
||||
// 拖拽图片镜像
|
||||
this.resizerImage.src = this.curElementSrc
|
||||
this.resizerImageContainer.style.display = 'block'
|
||||
const { coordinate: { leftTop: [left, top] } } = this.curPosition
|
||||
const prePageHeight = this.draw.getPageNo() * (height + pageGap)
|
||||
this.resizerImageContainer.style.left = `${left}px`
|
||||
this.resizerImageContainer.style.top = `${top + prePageHeight}px`
|
||||
this.resizerImage.style.width = `${this.curElement.width! * scale}px`
|
||||
this.resizerImage.style.height = `${this.curElement.height! * scale}px`
|
||||
// 追加全局事件
|
||||
const mousemoveFn = this._mousemove.bind(this)
|
||||
document.addEventListener('mousemove', mousemoveFn)
|
||||
document.addEventListener('mouseup', () => {
|
||||
// 改变尺寸
|
||||
if (this.curElement && this.curPosition) {
|
||||
this.curElement.width = this.width
|
||||
this.curElement.height = this.height
|
||||
this.draw.render({ isSetCursor: false })
|
||||
this.drawResizer(this.curElement, this.curPosition, this.previewerDrawOption)
|
||||
}
|
||||
// 还原副作用
|
||||
this.resizerImageContainer.style.display = 'none'
|
||||
document.removeEventListener('mousemove', mousemoveFn)
|
||||
document.body.style.cursor = ''
|
||||
this.canvas.style.cursor = 'text'
|
||||
}, {
|
||||
once: true
|
||||
})
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
private _mousemove(evt: MouseEvent) {
|
||||
if (!this.curElement) return
|
||||
const { scale } = this.options
|
||||
let dx = 0
|
||||
let dy = 0
|
||||
switch (this.curHandleIndex) {
|
||||
case 0:
|
||||
dx = this.mousedownX - evt.x
|
||||
dy = this.mousedownY - evt.y
|
||||
break
|
||||
case 1:
|
||||
dy = this.mousedownY - evt.y
|
||||
break
|
||||
case 2:
|
||||
dx = evt.x - this.mousedownX
|
||||
dy = this.mousedownY - evt.y
|
||||
break
|
||||
case 3:
|
||||
dx = evt.x - this.mousedownX
|
||||
break
|
||||
case 5:
|
||||
dy = evt.y - this.mousedownY
|
||||
break
|
||||
case 6:
|
||||
dx = this.mousedownX - evt.x
|
||||
dy = evt.y - this.mousedownY
|
||||
break
|
||||
case 7:
|
||||
dx = this.mousedownX - evt.x
|
||||
break
|
||||
default:
|
||||
dx = evt.x - this.mousedownX
|
||||
dy = evt.y - this.mousedownY
|
||||
break
|
||||
}
|
||||
this.width = this.curElement.width! + dx
|
||||
this.height = this.curElement.height! + dy
|
||||
this.resizerImage.style.width = `${this.width * scale}px`
|
||||
this.resizerImage.style.height = `${this.height * scale}px`
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
private _dblclick() {
|
||||
this._drawPreviewer()
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
private _drawPreviewer() {
|
||||
const previewerContainer = document.createElement('div')
|
||||
previewerContainer.classList.add('image-previewer')
|
||||
// 关闭按钮
|
||||
const closeBtn = document.createElement('i')
|
||||
closeBtn.classList.add('image-close')
|
||||
closeBtn.onclick = () => {
|
||||
this._clearPreviewer()
|
||||
}
|
||||
previewerContainer.append(closeBtn)
|
||||
// 图片
|
||||
const imgContainer = document.createElement('div')
|
||||
imgContainer.classList.add('image-container')
|
||||
const img = document.createElement('img')
|
||||
img.src = this.curElementSrc
|
||||
img.draggable = false
|
||||
imgContainer.append(img)
|
||||
this.previewerImage = img
|
||||
previewerContainer.append(imgContainer)
|
||||
// 操作栏
|
||||
let x = 0
|
||||
let y = 0
|
||||
let scaleSize = 1
|
||||
let rotateSize = 0
|
||||
const menuContainer = document.createElement('div')
|
||||
menuContainer.classList.add('image-menu')
|
||||
const zoomIn = document.createElement('i')
|
||||
zoomIn.classList.add('zoom-in')
|
||||
zoomIn.onclick = () => {
|
||||
scaleSize += 0.1
|
||||
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
|
||||
}
|
||||
menuContainer.append(zoomIn)
|
||||
const zoomOut = document.createElement('i')
|
||||
zoomOut.onclick = () => {
|
||||
if (scaleSize - 0.1 <= 0.1) return
|
||||
scaleSize -= 0.1
|
||||
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
|
||||
}
|
||||
zoomOut.classList.add('zoom-out')
|
||||
menuContainer.append(zoomOut)
|
||||
const rotate = document.createElement('i')
|
||||
rotate.classList.add('rotate')
|
||||
rotate.onclick = () => {
|
||||
rotateSize += 1
|
||||
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
|
||||
}
|
||||
menuContainer.append(rotate)
|
||||
const originalSize = document.createElement('i')
|
||||
originalSize.classList.add('original-size')
|
||||
originalSize.onclick = () => {
|
||||
x = 0
|
||||
y = 0
|
||||
scaleSize = 1
|
||||
rotateSize = 0
|
||||
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
|
||||
}
|
||||
menuContainer.append(originalSize)
|
||||
const imageDownload = document.createElement('i')
|
||||
imageDownload.classList.add('image-download')
|
||||
imageDownload.onclick = () => {
|
||||
const { mime } = this.previewerDrawOption
|
||||
downloadFile(img.src, `${this.curElement?.id}.${mime || 'png'}`)
|
||||
}
|
||||
menuContainer.append(imageDownload)
|
||||
previewerContainer.append(menuContainer)
|
||||
this.previewerContainer = previewerContainer
|
||||
document.body.append(previewerContainer)
|
||||
// 拖拽调整位置
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let isAllowDrag = false
|
||||
img.onmousedown = (evt) => {
|
||||
isAllowDrag = true
|
||||
startX = evt.x
|
||||
startY = evt.y
|
||||
previewerContainer.style.cursor = 'move'
|
||||
}
|
||||
previewerContainer.onmousemove = (evt: MouseEvent) => {
|
||||
if (!isAllowDrag) return
|
||||
x += (evt.x - startX)
|
||||
y += (evt.y - startY)
|
||||
startX = evt.x
|
||||
startY = evt.y
|
||||
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
|
||||
}
|
||||
previewerContainer.onmouseup = () => {
|
||||
isAllowDrag = false
|
||||
previewerContainer.style.cursor = 'auto'
|
||||
}
|
||||
previewerContainer.onwheel = (evt) => {
|
||||
evt.preventDefault()
|
||||
if (evt.deltaY < 0) {
|
||||
// 放大
|
||||
scaleSize += 0.1
|
||||
} else {
|
||||
// 缩小
|
||||
if (scaleSize - 0.1 <= 0.1) return
|
||||
scaleSize -= 0.1
|
||||
}
|
||||
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
|
||||
}
|
||||
}
|
||||
|
||||
public _setPreviewerTransform(scale: number, rotate: number, x: number, y: number) {
|
||||
if (!this.previewerImage) return
|
||||
this.previewerImage.style.left = `${x}px`
|
||||
this.previewerImage.style.top = `${y}px`
|
||||
this.previewerImage.style.transform = `scale(${scale}) rotate(${rotate * 90}deg)`
|
||||
}
|
||||
|
||||
private _clearPreviewer() {
|
||||
this.previewerContainer?.remove()
|
||||
this.previewerContainer = null
|
||||
document.body.style.overflow = 'auto'
|
||||
}
|
||||
|
||||
public drawResizer(element: IElement, position: IElementPosition, options: IPreviewerDrawOption = {}) {
|
||||
this.previewerDrawOption = options
|
||||
const { scale } = this.options
|
||||
const { coordinate: { leftTop: [left, top] } } = position
|
||||
const elementWidth = element.width! * scale
|
||||
const elementHeight = element.height! * scale
|
||||
const height = this.draw.getHeight()
|
||||
const pageGap = this.draw.getPageGap()
|
||||
const handleSize = this.options.resizerSize
|
||||
const preY = this.draw.getPageNo() * (height + pageGap)
|
||||
// 边框
|
||||
this.resizerSelection.style.left = `${left}px`
|
||||
this.resizerSelection.style.top = `${top + preY}px`
|
||||
this.resizerSelection.style.width = `${elementWidth}px`
|
||||
this.resizerSelection.style.height = `${elementHeight}px`
|
||||
// handle
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const left = i === 0 || i === 6 || i === 7
|
||||
? -handleSize
|
||||
: i === 1 || i === 5
|
||||
? elementWidth / 2
|
||||
: elementWidth - handleSize
|
||||
const top = i === 0 || i === 1 || i === 2
|
||||
? -handleSize
|
||||
: i === 3 || i === 7
|
||||
? elementHeight / 2 - handleSize
|
||||
: elementHeight - handleSize
|
||||
this.resizerHandleList[i].style.left = `${left}px`
|
||||
this.resizerHandleList[i].style.top = `${top}px`
|
||||
}
|
||||
this.resizerSelection.style.display = 'block'
|
||||
this.curElement = element
|
||||
this.curElementSrc = element[options.srcKey || 'value'] || ''
|
||||
this.curPosition = position
|
||||
this.width = this.curElement.width! * scale
|
||||
this.height = this.curElement.height! * scale
|
||||
}
|
||||
|
||||
public clearResizer() {
|
||||
this.resizerSelection.style.display = 'none'
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { IElement } from './Element'
|
||||
|
||||
export interface IPreviewerCreateResult {
|
||||
resizerSelection: HTMLDivElement;
|
||||
resizerHandleList: HTMLDivElement[];
|
||||
resizerImageContainer: HTMLDivElement;
|
||||
resizerImage: HTMLImageElement;
|
||||
}
|
||||
|
||||
export interface IPreviewerDrawOption {
|
||||
mime?: 'png' | 'jpg' | 'jpeg' | 'svg';
|
||||
srcKey?: keyof Pick<IElement, 'value' | 'laTexSVG'>;
|
||||
}
|
||||
Loading…
Reference in new issue