UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

268 lines (199 loc) • 7.22 kB
import { assert } from "../../../core/assert.js"; import { isValueBetween } from "../../../core/math/interval/isValueBetween.js"; import { PI2 } from "../../../core/math/PI2.js"; import SVG, { svgCircularPath } from "../../SVG.js"; import View from "../../View.js"; let idCount = 0; function generateCurveId() { const r = `radial-text-curve${idCount}`; idCount++; return r; } let idShadowFilter = 0; /** * NOTE: adapted from https://www.w3.org/People/Dean/svg/texteffects/shadow.svg * @param {number} dx * @param {number} dy * @param {number} blurX * @param {number} blurY * @returns {{el: Element, id: string}} */ function createShadowFilter(dx, dy, blurX, blurY) { const filter = SVG.createElement('filter'); const id = `drop-shadow-filter-${idShadowFilter++}`; filter.setAttribute('id', id); filter.setAttribute('x', '-20%'); filter.setAttribute('y', '-20%'); filter.setAttribute('width', '140%'); filter.setAttribute('height', '140%'); const feGaussianBlur = SVG.createElement('feGaussianBlur'); feGaussianBlur.setAttribute('stdDeviation', `${blurX} ${blurY}`); feGaussianBlur.setAttribute('result', 'shadow'); filter.appendChild(feGaussianBlur); if (dx !== 0 || dy !== 0) { const feOffset = SVG.createElement('feOffset'); filter.appendChild(feOffset); } return { el: filter, id }; } export class RadialText extends View { /** * * @param {number} share * @param {number} offset * @param {number} radius * @param {String|number} [fill] CSS color for the text * @param {boolean} [useShadow=true] * @param {boolean} [crop=true] If enabled - will crop the text to make sure it fits in the given section */ constructor({ share = 1, offset = 0, radius = 100, fill = 'black', useShadow = true, crop = true } = {}) { super(); assert.isNumber(share, "share"); assert.isNumber(offset, "offset"); assert.isNumber(radius, "radius"); this.share = share; this.offset = offset; this.radius = radius; /** * When text is flipped, this is added to the radius to move text further away from origin * NOTE: this is a fudge factor and need to be adjusted in tandem with font size * @type {number} */ this.flipAlignmentOffset = 20; /** * Setting this to true will automatically toggle flipY on and off based on the angle at which the text appears * @type {boolean} */ this.autoFlipY = true; /** * Text will be flipped up-side-down if set to true. If {@link #autoFlipY} is set to true, this parameter will be ignored and overridden * @type {boolean} */ this.flipY = false; /** * @readonly * @type {boolean} */ this.useShadow = useShadow; /** * * @type {boolean} */ this.crop = crop; const elSVG = SVG.createElement('svg'); elSVG.classList.add('radial-text-label'); const elPath = SVG.createElement('path'); const elText = SVG.createElement('text'); const elTextPath = SVG.createElement('textPath'); /** * NOTE: A unique curve ID is required as Chrome seems to pick up curves from other elements if IDs match * @type {string} */ const curveId = generateCurveId(); // elPath.setAttribute("id", curveId); elTextPath.setAttribute("startOffset", "50%"); elTextPath.setAttribute("text-anchor", "middle"); elTextPath.setAttributeNS('http://www.w3.org/1999/xlink', 'href', "#" + curveId); //assemble const elements = { path: elPath, textPath: elTextPath }; this.elements = elements; if (useShadow) { const shadowFilter = createShadowFilter(0, 0, 4, 4); const elDefs = SVG.createElement('defs'); elSVG.appendChild(elDefs); elDefs.appendChild(shadowFilter.el); const elShadowText = SVG.createElement('textPath'); elShadowText.setAttribute("startOffset", "50%"); elShadowText.setAttribute("text-anchor", "middle"); elShadowText.setAttributeNS('http://www.w3.org/1999/xlink', 'href', "#" + curveId); elShadowText.style.filter = `url(#${shadowFilter.id})`; elText.appendChild(elShadowText); elements.shadow = { textPath: elShadowText }; } elText.appendChild(elTextPath); elSVG.appendChild(elPath); elSVG.appendChild(elText); this.el = elSVG; this.setFill(fill); } /** * * @param {String|number} color */ setFill(color) { this.elements.textPath.setAttribute('fill', color); } /** * * @param {String} value */ setText(value) { let text = value; if (this.crop) { //figure out how many letters will fit const arcLength = 2 * Math.PI * this.radius * this.share; const charLength = 14; const maxLetters = arcLength / charLength; if (maxLetters < value.length) { if (maxLetters <= 3) { text = value.slice(0, maxLetters); } else { text = value.slice(0, maxLetters - 3) + '...'; } } } //set text value this.elements.textPath.textContent = text; if (this.useShadow) { this.elements.shadow.textPath.textContent = text; } } render() { const offset = this.offset; const share = this.share; let radius = this.radius; if (this.autoFlipY) { //find mid point of the arch let midPoint = offset + share / 2; if (midPoint < 0) { midPoint++; } this.flipY = isValueBetween(midPoint % 1, 0, 0.5); } let a0 = offset * PI2; let a1 = (offset + share) * PI2; let angle0, angle1; if (this.flipY) { angle0 = a1; angle1 = a0; //push the text off-center along the radius radius = radius + this.flipAlignmentOffset; } else { angle0 = a0; angle1 = a1; } const d = svgCircularPath( radius, angle0, angle1 ); // const d = "M6,150C49.63,93,105.79,36.65,156.2,47.55,207.89,58.74,213,131.91,264,150c40.67,14.43,108.57-6.91,229-145"; this.elements.path.setAttribute("d", d); } }