UNPKG

scrawl-canvas

Version:

Responsive, interactive and more accessible HTML5 canvas elements. Scrawl-canvas is a JavaScript library designed to make using the HTML5 canvas element easier, and more fun

1,025 lines (720 loc) 27.9 kB
// # Label factory // TODO - document purpose and description // #### Imports import { constructors, styles, stylesnames } from '../core/library.js'; import { doCreate, generateIdForArtefact, mergeOver, λnull, Ωempty } from '../helper/utilities.js'; import { makeState } from '../untracked-factory/state.js'; import { makeTextStyle } from '../untracked-factory/text-style.js'; import { currentGroup } from '../factory/canvas.js'; import { checkForWorkstoreItem, getWorkstoreItem } from '../helper/workstore.js'; import { gradientEngine } from '../helper/gradient-engine.js'; import { releaseCell, requestCell } from '../untracked-factory/cell-fragment.js'; import baseMix from '../mixin/base.js'; import entityMix from '../mixin/entity.js'; import textMix from '../mixin/text.js'; // Shared constants import { _abs, _ceil, _isArray, _isFinite, ALPHABETIC, BLACK, BOTTOM, CENTER, DESTINATION_OUT, END, ENTITY, FILL, GRADIENTS_ARR, HANGING, IDEOGRAPHIC, LEFT, LTR, MIDDLE, MOUSE, PARTICLE, RIGHT, ROUND, SOURCE_OUT, SOURCE_OVER, START, T_COLOR, T_LABEL, TOP, ZERO_STR } from '../helper/shared-vars.js'; // Local constants const METHODS_USING_DRAW_FIRST = ['drawAndFill', 'drawThenFill'], METHODS_USING_DRAW_LAST = ['draw', 'fillAndDraw', 'fillThenDraw'], DRAW = 'draw'; // #### Label constructor const Label = function (items = Ωempty) { this.entityInit(items); return this; }; // #### Label prototype const P = Label.prototype = doCreate(); P.type = T_LABEL; P.lib = ENTITY; P.isArtefact = true; P.isAsset = false; // #### Mixins baseMix(P); entityMix(P); textMix(P); // #### Label attributes const defaultAttributes = { // __text__ - string. text: ZERO_STR, showBoundingBox: false, boundingBoxStyle: BLACK, boundingBoxLineWidth: 1, boundingBoxLineDash: null, boundingBoxLineDashOffset: 0, useTextStyleForUnderline: false, useTextStyleForBoundingBox: false, useTextStyleForOutline: false, }; P.defs = mergeOver(P.defs, defaultAttributes); // #### Packet management // No additional packet management functionality required // #### Clone management // No additional clone functionality required // #### Kill management // No additional kill functionality required // #### Get, Set, deltaSet const G = P.getters, S = P.setters, D = P.deltaSetters; // __Note that__ dimensions (width, height) cannot be set on labels as the entity's dimensional values will depend entirely on the `font`, `text` and `scale` attributes G.width = function () { return this.currentDimensions[0]; }; S.width = λnull; D.width = λnull; G.height = function () { return this.currentDimensions[1]; }; S.height = λnull; D.height = λnull; G.dimensions = function () { return [...this.currentDimensions]; }; S.dimensions = λnull; D.dimensions = λnull; S.showBoundingBox = function (item) { this.showBoundingBox = !!item; this.dirtyFillGradientCache = true; }; S.boundingBoxStyle = function (item) { this.boundingBoxStyle = item; this.dirtyFillGradientCache = true; }; S.boundingBoxLineWidth = function (item) { if (_isFinite(item)) { this.boundingBoxLineWidth = item; this.dirtyFillGradientCache = true; } }; D.boundingBoxLineWidth = function (item) { if (_isFinite(item)) { this.boundingBoxLineWidth += item; this.dirtyFillGradientCache = true; } }; S.boundingBoxLineDash = function (item) { if (_isArray(item)) { this.boundingBoxLineDash = item; this.dirtyFillGradientCache = true; } }; S.boundingBoxLineDashOffset = function (item) { if (_isFinite(item)) { this.boundingBoxLineDashOffset = item; this.dirtyFillGradientCache = true; } }; D.boundingBoxLineDashOffset = function (item) { if (_isFinite(item)) { this.boundingBoxLineDashOffset += item; this.dirtyFillGradientCache = true; } }; S.useTextStyleForUnderline = function (item) { this.useTextStyleForUnderline = !!item; this.dirtyFillGradientCache = true; }; S.useTextStyleForBoundingBox = function (item) { this.useTextStyleForBoundingBox = !!item; this.dirtyFillGradientCache = true; }; S.useTextStyleForOutline = function (item) { this.useTextStyleForOutline = !!item; this.dirtyFillGradientCache = true; }; // #### Prototype functions // `entityInit` - overwrites the mixin/entity.js function P.entityInit = function (items = Ωempty) { this.modifyConstructorInputForAnchorButton(items); this.makeName(items.name); this.register(); this.initializePositions(); this.state = makeState(Ωempty); this.defaultTextStyle = makeTextStyle({ name: `${this.name}_default-textstyle`, isDefaultTextStyle: true, }); this.filters = []; this.currentFilters = []; this.dirtyFilters = false; this.dirtyFiltersCache = false; this.dirtyImageSubscribers = false; this.accessibleTextHold = null; this.accessibleTextHoldAttached = null; this.set(this.defs); if (!items.group) items.group = currentGroup; this.onEnter = λnull; this.onLeave = λnull; this.onDown = λnull; this.onUp = λnull; this.currentFontIsLoaded = false; this.updateUsingFontParts = false; this.updateUsingFontString = false; this.usingViewportFontSizing = true; this.letterSpaceValue = 0; this.wordSpaceValue = 0; this.alphabeticBaseline = 0; this.hangingBaseline = 0; this.ideographicBaseline = 0; this.fontVerticalOffset = 0; this.delta = {}; this.set(items); this.midInitActions(items); this.dirtyFont = true; this.currentFontIsLoaded = false; }; // `measureFont` - gather font metadata (uses `getFontMetadata` from text mixin) P.measureFont = function () { const { defaultTextStyle, currentScale, dimensions } = this; const { fontFamily, fontSizeValue, letterSpaceValue, wordSpaceValue } = defaultTextStyle; defaultTextStyle.letterSpacing = `${letterSpaceValue * currentScale}px`; defaultTextStyle.wordSpacing = `${wordSpaceValue * currentScale}px`; const mycell = requestCell(); const engine = mycell.engine; engine.font = defaultTextStyle.canvasFont; engine.fontKerning = defaultTextStyle.fontKerning; engine.fontStretch = defaultTextStyle.fontStretch; engine.fontVariantCaps = defaultTextStyle.fontVariantCaps; engine.textRendering = defaultTextStyle.textRendering; engine.letterSpacing = defaultTextStyle.letterSpacing; engine.wordSpacing = defaultTextStyle.wordSpacing; engine.direction = defaultTextStyle.direction; engine.textAlign = LEFT; engine.textBaseline = TOP; const metrics = engine.measureText(this.text); releaseCell(mycell); const { actualBoundingBoxLeft, actualBoundingBoxRight } = metrics; const meta = this.getFontMetadata(fontFamily); const ratio = fontSizeValue / 100; // If spacing is active, compute width manually (fallback) let width = _ceil(_abs(actualBoundingBoxLeft) + _abs(actualBoundingBoxRight)); if ((letterSpaceValue !== 0 || wordSpaceValue !== 0) && this.text) { const cell2 = requestCell(); const ctx = cell2.engine; ctx.font = defaultTextStyle.canvasFont; ctx.textAlign = LEFT; ctx.textBaseline = TOP; let total = 0; const t = this.text; for (let i = 0; i < t.length; i++) { const ch = t[i]; total += ctx.measureText(ch).width; if (i < t.length - 1) total += (letterSpaceValue + (ch === ' ' ? wordSpaceValue : 0)) * currentScale; } width = _ceil(total); releaseCell(cell2); } if (dimensions) { dimensions[0] = width; dimensions[1] = meta.height * ratio * currentScale; } const offset = meta.verticalOffset * ratio; this.alphabeticBaseline = ((meta.alphabeticBaseline * ratio) + offset) * currentScale; this.hangingBaseline = ((meta.hangingBaseline * ratio) + offset) * currentScale; this.ideographicBaseline = ((meta.ideographicBaseline * ratio) + offset) * currentScale; this.fontVerticalOffset = offset; this.dirtyPathObject = true; this.dirtyDimensions = true; // Tacky fix for making the dimensions update this.set({ letterSpacing: defaultTextStyle.letterSpacing }); }; // #### Clean functions // `cleanPathObject` - calculate the Label entity's __Path2D object__ P.cleanPathObject = function () { this.dirtyPathObject = false; const p = this.pathObject = new Path2D(); const handle = this.currentHandle, dims = this.currentDimensions; const [x, y] = handle; const [w, h] = dims; p.rect(-x, -y, w, h); }; // `cleanDimensions` - calculate the entity's __currentDimensions__ Array P.cleanDimensions = function () { this.dirtyDimensions = false; const dims = this.dimensions, curDims = this.currentDimensions; const [oldW, oldH] = curDims; curDims[0] = dims[0]; curDims[1] = dims[1]; this.dirtyStart = true; this.dirtyHandle = true; this.dirtyOffset = true; if (oldW !== curDims[0] || oldH !== curDims[1]) this.dirtyPositionSubscribers = true; if (this.mimicked && this.mimicked.length) this.dirtyMimicDimensions = true; this.dirtyFilterIdentifier = true; }; P.cleanHandle = function () { this.dirtyHandle = false; const { handle, currentHandle, currentDimensions, mimicked, defaultTextStyle, alphabeticBaseline, hangingBaseline, ideographicBaseline } = this; const [hx, hy] = handle; const [dx, dy] = currentDimensions; const direction = defaultTextStyle.direction || LTR; // horizontal if (hx.toFixed) currentHandle[0] = hx; else if (hx === LEFT) currentHandle[0] = 0; else if (hx === RIGHT) currentHandle[0] = dx; else if (hx === CENTER) currentHandle[0] = dx / 2; else if (hx === START) currentHandle[0] = (direction === LTR) ? 0 : dx; else if (hx === END) currentHandle[0] = (direction === LTR) ? dx : 0; else if (!_isFinite(parseFloat(hx))) currentHandle[0] = 0; else currentHandle[0] = (parseFloat(hx) / 100) * dx; // vertical if (hy.toFixed) currentHandle[1] = hy; else if (hy === TOP) currentHandle[1] = 0; else if (hy === BOTTOM) currentHandle[1] = dy; else if (hy === CENTER) currentHandle[1] = dy / 2; else if (hy === MIDDLE) currentHandle[1] = dy / 2; else if (hy === HANGING) currentHandle[1] = (_isFinite(hangingBaseline)) ? hangingBaseline : 0; else if (hy === ALPHABETIC) currentHandle[1] = (_isFinite(alphabeticBaseline)) ? alphabeticBaseline : 0; else if (hy === IDEOGRAPHIC) currentHandle[1] = (_isFinite(ideographicBaseline)) ? ideographicBaseline : 0; else if (!_isFinite(parseFloat(hy))) currentHandle[1] = 0; else currentHandle[1] = (parseFloat(hy) / 100) * dy; this.dirtyFilterIdentifier = true; this.dirtyStampHandlePositions = true; if (mimicked && mimicked.length) this.dirtyMimicHandle = true; }; // Overwrites the function in mixin/entity.js P.setGradientCacheFlags = function () { let fillGradient; if (this.state) { const fillStyle = this.state.fillStyle; fillGradient = fillStyle.substring ? styles[fillStyle] : fillStyle; } this.useFillGradientCache = (fillGradient && GRADIENTS_ARR.includes(fillGradient.type)); this.useDrawGradientCache = false; }; // #### Display cycle functions P.prepareStamp = function() { if (this.dirtyHost) this.dirtyHost = false; this.setGradientCacheFlags(); if (this.dirtyScale || this.dirtyDimensions || this.dirtyStart || this.dirtyOffset || this.dirtyHandle) { this.dirtyPathObject = true; if (this.useFillGradientCache) this.dirtyFillGradientCache = true; } if (this.dirtyRotation) { if (this.useFillGradientCache) this.dirtyFillGradientCache = true; } if (this.dirtyScale) this.cleanScale(); if (this.dirtyText) this.updateAccessibleTextHold(); if (this.dirtyFont) this.cleanFont(); if (this.dirtyDimensions) this.cleanDimensions(); if (this.dirtyLock) this.cleanLock(); if (this.dirtyStart) this.cleanStart(); if (this.dirtyOffset) this.cleanOffset(); if (this.dirtyHandle) this.cleanHandle(); if (this.dirtyRotation) this.cleanRotation(); if (this.isBeingDragged || this.lockTo.includes(MOUSE) || this.lockTo.includes(PARTICLE)) { this.dirtyStampPositions = true; this.dirtyStampHandlePositions = true; } if (this.dirtyStampPositions || this.dirtyStampHandlePositions) { if (this.useFillGradientCache) this.dirtyFillGradientCache = true; } if (this.dirtyStampPositions) this.cleanStampPositions(); if (this.dirtyStampHandlePositions) this.cleanStampHandlePositions(); if (this.dirtyPathObject) this.cleanPathObject(); if (this.dirtyPositionSubscribers) this.updatePositionSubscribers(); this.prepareStampTabsHelper(); }; // ##### Stamp methods // `regularStamp` - overwrites mixin/entity.js function. P.regularStamp = function () { const dest = this.currentHost, textStyle = this.defaultTextStyle; if (dest && textStyle && this.currentFontIsLoaded) { const engine = dest.engine; const [x, y] = this.currentStampPosition; dest.rotateDestination(engine, x, y, this); if (!this.noCanvasEngineUpdates) { this.state.set(textStyle); dest.setEngine(this); } if (this.useFillGradientCache) this.updateGradientsBeforeStamp(dest); this.setImageSmoothing(engine); const pos = this.stampPositioningHelper(); if (this.useFillGradientCache) this.stampLabelGradientFill(dest); else this.stampLabelDirect(engine, pos); this.postProcessLabelDecorations(dest, pos); } }; P.updateGradientsBeforeStamp = function (refCell) { if (!this.useFillGradientCache) return; const getFixedGradientData = this.getFixedGradientData.bind(this); const state = this.state; const grad = (state.fillStyle.substring) ? styles[state.fillStyle] : state.fillStyle; if (!grad || !GRADIENTS_ARR.includes(grad.type)) return; let fillId = this.identifierFillGradientCache; if (this.dirtyFillGradient || this.dirtyFillGradientCache || fillId === ZERO_STR) fillId = generateIdForArtefact(this); if (!fillId) return; if (checkForWorkstoreItem(fillId)) return; const myCell = requestCell(), element = myCell.element, engine = myCell.engine; const [width, height] = refCell.get('dimensions'); const [x, y] = this.currentStampPosition; const matrix = refCell.engine.getTransform(); element.width = width; element.height = height; myCell.rotateDestination(engine, x, y, this); myCell.setEngine(this); this.setLabelTextEngine(engine); // Gradients don't understand the quirks of Label entity scaling const tempScale = this.currentScale; this.currentScale = 1; grad.getData(this, refCell, FILL); this.currentScale = tempScale; this.setImageSmoothing(engine); const pos = this.stampPositioningHelper(); engine.fillText(...pos); if (this.useTextStyleForOutline) engine.strokeText(...pos); if (this.defaultTextStyle && this.defaultTextStyle.includeUnderline && this.useTextStyleForUnderline) { this.addUnderlineToGradientMask(myCell, pos, matrix); } if (this.showBoundingBox && this.useTextStyleForBoundingBox && this.pathObject) { engine.save(); engine.lineWidth = this.boundingBoxLineWidth; engine.setLineDash(this.boundingBoxLineDash || []); engine.lineDashOffset = this.boundingBoxLineDashOffset || 0; engine.stroke(this.pathObject); engine.restore(); } const data = engine.getImageData(0, 0, width, height); // Sadly, the gradient engine doesn't understand the quirks of Label entity scaling either this.currentScale = 1; const success = gradientEngine.action({ fixedGradientData: getFixedGradientData(grad, FILL), identifier: fillId, imageData: data, entity: this, matrix, }); this.currentScale = tempScale; if (success) { this.identifierFillGradientCache = fillId; this.dirtyFillGradient = false; this.dirtyFillGradientCache = false; this.dirtyFilterIdentifier = true; } else this.dirtyFillGradientCache = true; releaseCell(myCell); }; P.setLabelTextEngine = function (engine) { const textStyle = this.defaultTextStyle; engine.font = textStyle.canvasFont; engine.fontKerning = textStyle.fontKerning; engine.fontStretch = textStyle.fontStretch; engine.fontVariantCaps = textStyle.fontVariantCaps; engine.textRendering = textStyle.textRendering; engine.letterSpacing = textStyle.letterSpacing; engine.wordSpacing = textStyle.wordSpacing; engine.direction = textStyle.direction; engine.textAlign = LEFT; engine.textBaseline = TOP; engine.shadowOffsetX = 0; engine.shadowOffsetY = 0; engine.shadowBlur = 0; engine.fillStyle = BLACK; engine.strokeStyle = BLACK; }; P.stampLabelDirect = function (engine, pos) { const method = this.defaultTextStyle.method, complexFill = this.useFillGradientCache, textFillColor = this.getLabelTextFillColor(); if ( (!complexFill || !this.useTextStyleForOutline) && this.methodDrawsOutlineBeforeFill(method) ) { this.drawLabelOutline( engine, pos, this.useTextStyleForOutline ? textFillColor : null ); } if (method !== DRAW) { engine.fillStyle = textFillColor; engine.fillText(...pos); } }; P.stampLabelGradientFill = function (host) { const identifier = this.identifierFillGradientCache; if (!identifier) return; const data = getWorkstoreItem(identifier); if (data && data.w && data.h && data.imageData) { const { x, y, w, h, imageData } = data; const myCell = requestCell(); const { element, engine, } = myCell; element.width = w; element.height = h; engine.putImageData(imageData, 0, 0); const hostEngine = host.engine; hostEngine.save(); hostEngine.resetTransform(); hostEngine.drawImage(element, x, y); hostEngine.restore(); releaseCell(myCell); } }; P.addUnderlineToGradientMask = function (targetCell, pos) { const { currentDimensions, currentScale, currentStampPosition, defaultTextStyle, fontVerticalOffset, } = this; const { underlineGap, underlineOffset, underlineWidth, } = defaultTextStyle; const [, x, y] = pos; const [localWidth, localHeight] = currentDimensions; const underlineStartY = y + (underlineOffset * localHeight) - fontVerticalOffset * currentScale; const underlineDepth = underlineWidth * currentScale; const { element: targetElement, engine: targetEngine, } = targetCell; const mycell = requestCell(targetElement.width, targetElement.height); const { element, engine, } = mycell; element.width = targetElement.width; element.height = targetElement.height; mycell.rotateDestination(engine, ...currentStampPosition, this); engine.fillStyle = BLACK; engine.strokeStyle = BLACK; engine.font = defaultTextStyle.canvasFont; engine.fontKerning = defaultTextStyle.fontKerning; engine.fontStretch = defaultTextStyle.fontStretch; engine.fontVariantCaps = defaultTextStyle.fontVariantCaps; engine.textRendering = defaultTextStyle.textRendering; engine.letterSpacing = defaultTextStyle.letterSpacing; engine.lineCap = ROUND; engine.lineJoin = ROUND; engine.wordSpacing = defaultTextStyle.wordSpacing; engine.direction = defaultTextStyle.direction; engine.textAlign = LEFT; engine.textBaseline = TOP; engine.lineWidth = (underlineGap * 2) * currentScale; this.setImageSmoothing(engine); engine.strokeText(...pos); engine.fillText(...pos); engine.globalCompositeOperation = SOURCE_OUT; engine.fillRect(x, underlineStartY, localWidth, underlineDepth); targetEngine.save(); targetEngine.resetTransform(); targetEngine.globalCompositeOperation = SOURCE_OVER; targetEngine.drawImage(element, 0, 0); targetEngine.restore(); releaseCell(mycell); }; // `stampPositioningHelper` - internal helper function P.stampPositioningHelper = function () { const { currentHandle, currentScale, text, fontVerticalOffset } = this; const x = -currentHandle[0], y = -currentHandle[1] + fontVerticalOffset * currentScale; return [text, x, y]; } // Overwrite stamp functions from mixin/entity.js P.draw = function (host) { const pos = this.stampPositioningHelper(); this.postProcessLabelDecorations(host, pos); }; P.fill = function (host) { this.stampLabelDirect(host.engine, this.stampPositioningHelper()); }; P.drawAndFill = P.fill; P.fillAndDraw = P.fill; P.drawThenFill = P.fill; P.fillThenDraw = P.fill; // `clip` - restrict drawing activities to the entity's enclosed area P.clip = function (host) { const engine = host.engine; engine.clip(this.pathObject, this.winding); }; // `clear` - remove everything that would have been covered if the entity had performed fill (including shadow) P.clear = function (host) { if (this.currentFontIsLoaded) { const engine = host.engine; const gco = engine.globalCompositeOperation; const pos = this.stampPositioningHelper(); engine.globalCompositeOperation = DESTINATION_OUT; engine.fillText(...pos); engine.globalCompositeOperation = gco; if (this.showBoundingBox) this.drawBoundingBox(host); } }; // `none` - perform all the calculations required, but don't perform the final stamping P.none = λnull; P.postProcessLabelDecorations = function (host, pos) { if (!this.currentFontIsLoaded) return; const engine = host.engine, textStyle = this.defaultTextStyle, complexFill = this.useFillGradientCache, textFillColor = this.getLabelTextFillColor(); if (!engine || !textStyle) return; if ( (!complexFill || !this.useTextStyleForOutline) && this.methodDrawsOutlineAfterFill(textStyle.method) ) { this.drawLabelOutline( engine, pos, this.useTextStyleForOutline ? textFillColor : null ); } if ( textStyle.includeUnderline && (!complexFill || !this.useTextStyleForUnderline) ) { this.underlineEngine( host, pos, this.useTextStyleForUnderline ? textFillColor : null ); } if ( this.showBoundingBox && (!complexFill || !this.useTextStyleForBoundingBox) ) { this.drawBoundingBox( host, this.useTextStyleForBoundingBox ? textFillColor : null ); } }; P.getLabelTextFillColor = function () { return this.getLabelColorOnlyStyle(this.state.fillStyle); }; P.methodDrawsOutlineBeforeFill = function (method) { return METHODS_USING_DRAW_FIRST.includes(method); }; P.methodDrawsOutlineAfterFill = function (method) { return METHODS_USING_DRAW_LAST.includes(method); }; P.drawLabelOutline = function (engine, pos, style = null) { if (this.state) { engine.save(); engine.strokeStyle = style || this.getLabelColorOnlyStyle(this.state.strokeStyle); engine.fillStyle = this.getLabelTextFillColor(); engine.shadowOffsetX = 0; engine.shadowOffsetY = 0; engine.shadowBlur = 0; this.setImageSmoothing(engine); engine.strokeText(...pos); engine.restore(); } }; P.getLabelColorOnlyStyle = function (item, fallback = BLACK) { if (!item) return fallback; if (item.substring) { if (stylesnames.includes(item)) { const obj = styles[item]; if (obj && obj.type === T_COLOR) return obj.get('color'); return fallback; } return item; } if (item.name && item.type && item.type === T_COLOR) return item.get(); return fallback; }; // `underlineEngine` - internal helper function P.underlineEngine = function (host, pos, style = null) { const { currentDimensions, currentScale, currentStampPosition, defaultTextStyle, fontVerticalOffset, } = this; const { underlineGap, underlineOffset, underlineStyle, underlineWidth, } = defaultTextStyle; const [, x, y] = pos; const [localWidth, localHeight] = currentDimensions; const underlineStartY = y + (underlineOffset * localHeight) - fontVerticalOffset * currentScale; const underlineDepth = underlineWidth * currentScale; const { element, engine } = host; const mycell = requestCell(element.width, element.height); const { element: underlineElement, engine: underlineEngine, } = mycell; mycell.rotateDestination(underlineEngine, ...currentStampPosition, this); underlineEngine.fillStyle = BLACK; underlineEngine.strokeStyle = BLACK; underlineEngine.font = defaultTextStyle.canvasFont; underlineEngine.fontKerning = defaultTextStyle.fontKerning; underlineEngine.fontStretch = defaultTextStyle.fontStretch; underlineEngine.fontVariantCaps = defaultTextStyle.fontVariantCaps; underlineEngine.textRendering = defaultTextStyle.textRendering; underlineEngine.letterSpacing = defaultTextStyle.letterSpacing; underlineEngine.lineCap = ROUND; underlineEngine.lineJoin = ROUND; underlineEngine.wordSpacing = defaultTextStyle.wordSpacing; underlineEngine.direction = defaultTextStyle.direction; underlineEngine.textAlign = LEFT; underlineEngine.textBaseline = TOP; underlineEngine.lineWidth = (underlineGap * 2) * currentScale; this.setImageSmoothing(underlineEngine); const uStyle = style || this.getLabelColorOnlyStyle(underlineStyle); underlineEngine.strokeText(...pos); underlineEngine.fillText(...pos); underlineEngine.globalCompositeOperation = 'source-out'; underlineEngine.fillStyle = uStyle; underlineEngine.fillRect(x, underlineStartY, localWidth, underlineDepth); engine.save(); engine.resetTransform(); this.setImageSmoothing(engine); engine.drawImage(underlineElement, 0, 0); engine.restore(); releaseCell(mycell); }; // `drawBoundingBox` - internal helper function called by `method` functions P.drawBoundingBox = function (host, style = null) { if (this.pathObject) { const uStroke = style || this.getLabelColorOnlyStyle(this.boundingBoxStyle); const engine = host.engine; engine.save(); engine.strokeStyle = uStroke; engine.lineWidth = this.boundingBoxLineWidth; engine.setLineDash(this.boundingBoxLineDash || []); engine.lineDashOffset = this.boundingBoxLineDashOffset || 0; engine.globalCompositeOperation = SOURCE_OVER; engine.globalAlpha = 1; engine.shadowOffsetX = 0; engine.shadowOffsetY = 0; engine.shadowBlur = 0; this.setImageSmoothing(engine); engine.stroke(this.pathObject); engine.restore(); } }; // #### Factory // ``` // scrawl.makeLabel({ // // name: 'mylabel-fill', // // }).clone({ // // name: 'mylabel-draw', // }); // ``` export const makeLabel = function (items) { if (!items) return false; return new Label(items); }; constructors.Label = Label;