UNPKG

@sutton-signwriting/font-ttf

Version:

a javascript package for node that generates SVG and PNG images for individual symbols, complete signs, and structured text. The package covers the entire set of the International SignWritnig Alphabet 2010 (ISWA 2010).

1,583 lines (1,524 loc) 147 kB
/** * Sutton SignWriting TrueType Font Module v1.6.0 (https://github.com/sutton-signwriting/font-ttf) * Author: Steve Slevinski (https://SteveSlevinski.me) * index.mjs is released under the MIT License. */ /** * Function that appends font-face CSS for the Sutton SignWriting fonts for system installed fonts, relative directory fonts, or content delivery network * @function font.cssAppend * @param {string} dir - an optional relative directory for font location * @example * font.cssAppend('./font/') */ const cssAppend = function (dir = '') { const id = "SgnwFontCss"; if (!document.getElementById(id)) { const style = document.createElement('style'); style.setAttribute("id", "SgnwFontCss"); style.appendChild(document.createTextNode(` @font-face { font-family: "SuttonSignWritingLine"; src: local('SuttonSignWritingLine'), ${dir ? `url('${dir}SuttonSignWritingLine.ttf') format('truetype'),` : ""} url('https://cdn.jsdelivr.net/npm/@sutton-signwriting/font-ttf@1.0.0/font/SuttonSignWritingLine.ttf') format('truetype'); } @font-face { font-family: "SuttonSignWritingFill"; src: local('SuttonSignWritingFill'), ${dir ? `url('${dir}SuttonSignWritingFill.ttf') format('truetype'),` : ""} url('https://cdn.jsdelivr.net/npm/@sutton-signwriting/font-ttf@1.0.0/font/SuttonSignWritingFill.ttf') format('truetype'); } @font-face { font-family: "SuttonSignWritingOneD"; src: local('SuttonSignWritingOneD'), ${dir ? `url('${dir}SuttonSignWritingOneD.ttf') format('truetype'),` : ""} url('https://cdn.jsdelivr.net/npm/@sutton-signwriting/font-ttf@1.0.0/font/SuttonSignWritingOneD.ttf') format('truetype'); } `)); document.head.appendChild(style); } }; let sizes = {}; const zoom = 2; const bound = 76 * zoom; let context; /** * Function that returns the size of a symbol using an id * @function font.symbolSize * @param {number} id - a 16-bit number of a symbol * @returns {number[]} width and height of symbol * @example * font.symbolSize(1) * * return [15,30] */ const symbolSize$2 = function (id) { if (id in sizes) { return [...sizes[id]]; } if (!context) { const canvaser = document.createElement("canvas"); canvaser.width = bound; canvaser.height = bound; context = canvaser.getContext("2d", { willReadFrequently: true }); } context.clearRect(0, 0, bound, bound); context.font = 30 * zoom + "px 'SuttonSignWritingLine'"; context.fillText(String.fromCodePoint(id + 0xF0000), 0, 0); const imgData = context.getImageData(0, 0, bound, bound).data; let w, h, i, s; wloop: for (w = bound - 1; w >= 0; w--) { for (h = 0; h < bound; h += 1) { for (s = 0; s < 4; s += 1) { i = w * 4 + h * 4 * bound + s; if (imgData[i]) { break wloop; } } } } var width = w; hloop: for (h = bound - 1; h >= 0; h--) { for (w = 0; w < width; w += 1) { for (s = 0; s < 4; s += 1) { i = w * 4 + h * 4 * bound + s; if (imgData[i]) { break hloop; } } } } var height = h + 1; width = Math.ceil(width / zoom); height = Math.ceil(height / zoom); // Rounding error in chrome. Manual fixes. if (14394 == id) { width = 19; } if ([10468, 10480, 10496, 10512, 10500, 10532, 10548, 10862, 10878, 10894, 11058, 11074, 11476, 11488, 11492, 11504, 11508, 11520, 10516, 10910, 10926, 11042, 11082, 10942].includes(id)) { width = 20; } if (31921 == id) { width = 22; } if (38460 == id) { width = 23; } if ([20164, 20212].includes(id)) { width = 25; } if (31894 == id) { width = 28; } if (46698 == id) { width = 29; } if (29606 == id) { width = 30; } if (44855 == id) { width = 40; } if (32667 == id) { width = 50; } if ([11088, 11474, 11490, 11506].includes(id)) { height = 20; } if (6285 == id) { height = 21; } if (40804 == id) { height = 31; } if (41475 == id) { height = 36; } // Error in chrome. Manual fix. // if (width==0 && height==0) { if (width == 0 && height == 0) { const sizefix = { 9: [15, 30], 10: [21, 30], 11: [30, 15], 12: [30, 21], 13: [15, 30], 14: [21, 30] }; if (id in sizefix) { width = sizefix[id][0]; height = sizefix[id][1]; } } if (width == 0 && height == 0) { return undefined; } sizes[id] = [width, height]; return [width, height]; }; /** * Function that returns a plane 15 character for a symbol line using an id * @function font.symbolLine * @param {number} id - a 16-bit number of a symbol * @returns {string} character for symbol line * @example * font.symbolLine(1) * * return '󰀁' */ const symbolLine$2 = function (id) { return String.fromCodePoint(id + 0xF0000); }; /** * Function that returns a plane 16 character for a symbol fill using an id * @function font.symbolFill * @param {number} id - a 16-bit number of a symbol * @returns {string} character for symbol fill * @example * font.symbolFill(1) * * return '􀀁' */ const symbolFill$2 = function (id) { return String.fromCodePoint(id + 0x100000); }; /** * Function that creates two text elements for a symbol using an id * @function font.symbolText * @param {number} id - a 16-bit number of a symbol * @returns {string} SVG segment for line and fill * @example * font.symbolText(1) * * return ` <text class="sym-fill" fill="white" style="pointer-events:none;font-family:'SuttonSignWritingFill';font-size:30px;">􀀁</text> * <text class="sym-line" fill="black" style="pointer-events:none;font-family:'SuttonSignWritingLine';font-size:30px;">󰀁</text>` */ const symbolText$2 = function (id) { return ` <text class="sym-fill" fill="white" style="pointer-events:none;font-family:'SuttonSignWritingFill';font-size:30px;">${symbolFill$2(id)}</text> <text class="sym-line" fill="black" style="pointer-events:none;font-family:'SuttonSignWritingLine';font-size:30px;">${symbolLine$2(id)}</text>`; }; /** * Function that executes a callback function once the Sutton SignWriiting Line and Fill fonts are ready to use * @function font.cssLoaded * @param {function} callback - a callback function to execute when fonts are ready * @example * const callback = () => { * console.log("Sutton SignWriting Line and Fill fonts are ready to use") * } * * font.cssLoaded( callback ) */ const cssLoaded = function (callback) { let lineReady = false; let fillReady = false; cssLoadedLine(() => { lineReady = true; }); cssLoadedFill(() => { fillReady = true; }); const cssCheck = setInterval(function () { if (lineReady && fillReady) { clearInterval(cssCheck); callback(); } }, 100); }; /** * Function that executes a callback function once the Sutton SignWriiting Line font is ready to use * @function font.cssLoadedLine * @param {function} callback - a callback function to execute when line font is ready * @example * const callback = () => { * console.log("Sutton SignWriting Line font is ready to use") * } * * font.cssLoadedLine( callback ) */ const cssLoadedLine = function (callback) { if (!symbolSize$2(1)) { const cssCheck = setInterval(function () { if (symbolSize$2(1)) { clearInterval(cssCheck); callback(); } }, 100); } else { callback(); } }; /** * Function that executes a callback function once the Sutton SignWriiting Fill font is ready to use * @function font.cssLoadedFill * @param {function} callback - a callback function to execute when fill font is ready * @example * const callback = () => { * console.log("Sutton SignWriting Fill font is ready to use") * } * * font.cssLoadedFill( callback ) */ const cssLoadedFill = function (callback) { const fillReady = function () { const canvaser = document.createElement("canvas"); canvaser.width = 15; canvaser.height = 30; const context = canvaser.getContext("2d"); context.font = "30px 'SuttonSignWritingFill'"; context.fillText(symbolFill$2(1), 0, 0); const imgData = context.getImageData(0, 0, 15, 30).data; return !imgData.every(item => item === 0); }; if (!fillReady()) { const cssCheck = setInterval(function () { if (fillReady()) { clearInterval(cssCheck); callback(); } }, 100); } else { callback(); } }; /** The font module contains functions for handing the TrueType fonts. * @module font */ var index$2 = /*#__PURE__*/Object.freeze({ __proto__: null, cssAppend: cssAppend, cssLoaded: cssLoaded, cssLoadedLine: cssLoadedLine, cssLoadedFill: cssLoadedFill, symbolSize: symbolSize$2, symbolLine: symbolLine$2, symbolFill: symbolFill$2, symbolText: symbolText$2 }); /** * Sutton SignWriting Core Module v2.0.0 (https://github.com/sutton-signwriting/core) * Author: Steve Slevinski (https://SteveSlevinski.me) * convert.mjs is released under the MIT License. */ /** * Function to convert a number to an SWU number character * @function convert.num2swu * @param {number} num - Integer value for number * @returns {string} SWU number character * @example * convert.num2swu(500) * * return '𝤆' */ const num2swu$1 = num => String.fromCodePoint(0x1D80C + parseInt(num) - 250); /** * Function to convert an array of x,y integers to two SWU number characters * @function convert.coord2swu * @param {number[]} coord - Array of x,y integers * @returns {string} Two SWU number character * @example * convert.coord2swu([500, 500]) * * return '𝤆𝤆' */ const coord2swu$1 = coord => coord.map(num => num2swu$1(num)).join(''); /** * Function to convert an SWU symbol character to a code point on plane 4 * @function convert.swu2code * @param {string} swuSym - SWU symbol character * @returns {number} Code point on plane 4 * @example * convert.swu2code('񀀁') * * return 0x40001 */ const swu2code$1 = swuSym => parseInt(swuSym.codePointAt(0)); /** * Function to convert a code point on plane 4 to an SWU symbol character * @function convert.code2swu * @param {number} code - Code point on plane 4 * @returns {string} SWU symbol character * @example * convert.code2swu(0x40001) * * return '񀀁' */ const code2swu = code => String.fromCodePoint(code); /** * Function to convert an SWU symbol character to a 16-bit ID * @function convert.swu2id * @param {string} swuSym - SWU symbol character * @returns {number} 16-bit ID * @example * convert.swu2id('񀀁') * * return 1 */ const swu2id = swuSym => swu2code$1(swuSym) - 0x40000; /** * Function to convert an FSW symbol key to a 16-bit ID * @function convert.key2id * @param {string} key - FSW symbol key * @returns {number} 16-bit ID * @example * convert.key2id('S10000') * * return 1 */ const key2id = key => key === "S00000" ? 0 : 1 + (parseInt(key.slice(1, 4), 16) - 256) * 96 + parseInt(key.slice(4, 5), 16) * 16 + parseInt(key.slice(5, 6), 16); /** * Function to convert an SWU symbol character to an FSW symbol key * @function convert.swu2key * @param {string} swuSym - SWU symbol character * @returns {string} FSW symbol key * @example * convert.swu2key('񀀁') * * return 'S10000' */ const swu2key = swuSym => { if (swuSym === "񀀀") { return "S00000"; } const symcode = swu2code$1(swuSym) - 0x40001; const base = parseInt(symcode / 96); const fill = parseInt((symcode - base * 96) / 16); const rotation = parseInt(symcode - base * 96 - fill * 16); return 'S' + (base + 0x100).toString(16) + fill.toString(16) + rotation.toString(16); }; /** * Function to convert an FSW symbol key to an SWU symbol character * @function convert.key2swu * @param {string} key - FSW symbol key * @returns {string} SWU symbol character * @example * convert.key2swu('S10000') * * return '񀀁' */ const key2swu = key => { if (key === "S00000") { return code2swu(0x40000); } return code2swu(0x40001 + (parseInt(key.slice(1, 4), 16) - 256) * 96 + parseInt(key.slice(4, 5), 16) * 16 + parseInt(key.slice(5, 6), 16)); }; /* support ongoing development */ /* https://patreon.com/signwriting */ /* https://donate.sutton-signwriting.io */ /** * Sutton SignWriting Core Module v2.0.0 (https://github.com/sutton-signwriting/core) * Author: Steve Slevinski (https://SteveSlevinski.me) * fsw.mjs is released under the MIT License. */ /** * Object of regular expressions for FSW strings * * @alias fsw.re * @property {string} null - the null symbol * @property {string} symbol - a symbol * @property {string} nullorsymbol - null or a symbol * @property {string} sort - the sorting marker * @property {string} prefix - a sorting marker followed by one or more symbols with nulls * @property {string} box - a signbox marker * @property {string} coord - a coordinate * @property {string} spatial - a symbol followed by a coordinate * @property {string} signbox - a signbox marker, max coordinate and zero or more spatial symbols * @property {string} sign - an optional prefix followed by a signbox * @property {string} sortable - a mandatory prefix followed by a signbox */ let re$1$1 = { 'null': 'S00000', 'symbol': 'S[123][0-9a-f]{2}[0-5][0-9a-f]', 'coord': '[0-9]{3}x[0-9]{3}', 'sort': 'A', 'box': '[BLMR]' }; re$1$1.nullorsymbol = `(?:${re$1$1.null}|${re$1$1.symbol})`; re$1$1.prefix = `(?:${re$1$1.sort}${re$1$1.nullorsymbol}+)`; re$1$1.spatial = `${re$1$1.symbol}${re$1$1.coord}`; re$1$1.signbox = `${re$1$1.box}${re$1$1.coord}(?:${re$1$1.spatial})*`; re$1$1.sign = `${re$1$1.prefix}?${re$1$1.signbox}`; re$1$1.sortable = `${re$1$1.prefix}${re$1$1.signbox}`; /** * Object of regular expressions for style strings * * @alias style.re * @type {object} * @property {string} colorize - regular expression for colorize section * @property {string} colorhex - regular expression for color hex values with 3 or 6 characters * @property {string} colorname - regular expression for css color name * @property {string} padding - regular expression for padding section * @property {string} zoom - regular expression for zoom section * @property {string} classbase - regular expression for class name definition * @property {string} id - regular expression for id definition * @property {string} colorbase - regular expression for color hex or color name * @property {string} color - regular expression for single color entry * @property {string} colors - regular expression for double color entry * @property {string} background - regular expression for background section * @property {string} detail - regular expression for color details for line and optional fill * @property {string} detailsym - regular expression for color details for individual symbols * @property {string} classes - regular expression for one or more class names * @property {string} full - full regular expression for style string */ let re$3 = { 'colorize': 'C', 'colorhex': '(?:[0-9a-fA-F]{3}){1,2}', 'colorname': '[a-zA-Z]+', 'padding': 'P[0-9]{2}', 'zoom': 'Z(?:[0-9]+(?:\\.[0-9]+)?|x)', 'classbase': '-?[_a-zA-Z][_a-zA-Z0-9-]{0,100}', 'id': '[a-zA-Z][_a-zA-Z0-9-]{0,100}' }; re$3.colorbase = `(?:${re$3.colorhex}|${re$3.colorname})`; re$3.color = `_${re$3.colorbase}_`; re$3.colors = `_${re$3.colorbase}(?:,${re$3.colorbase})?_`; re$3.background = `G${re$3.color}`; re$3.detail = `D${re$3.colors}`; re$3.detailsym = `D[0-9]{2}${re$3.colors}`; re$3.classes = `${re$3.classbase}(?: ${re$3.classbase})*`; re$3.full = `-(${re$3.colorize})?(${re$3.padding})?(${re$3.background})?(${re$3.detail})?(${re$3.zoom})?(?:-((?:${re$3.detailsym})*))?(?:-(${re$3.classes})?!(?:(${re$3.id})!)?)?`; const prefixColor$2 = color => { const regex = new RegExp(`^${re$3.colorhex}$`); return (regex.test(color) ? '#' : '') + color; }; const definedProps$2 = obj => Object.fromEntries(Object.entries(obj).filter(([k, v]) => v !== undefined)); /** * Function to parse style string to object * @function style.parse * @param {string} styleString - a style string * @returns {StyleObject} elements of style string * @example * style.parse('-CP10G_blue_D_red,Cyan_') * * return { * 'colorize': true, * 'padding': 10, * 'background': 'blue', * 'detail': ['red', 'Cyan'] * } */ const parse$1$1 = styleString => { const regex = `^${re$3.full}`; const m = (typeof styleString === 'string' ? styleString.match(new RegExp(regex)) : []) || []; return definedProps$2({ 'colorize': !m[1] ? undefined : !!m[1], 'padding': !m[2] ? undefined : parseInt(m[2].slice(1)), 'background': !m[3] ? undefined : prefixColor$2(m[3].slice(2, -1)), 'detail': !m[4] ? undefined : m[4].slice(2, -1).split(',').map(prefixColor$2), 'zoom': !m[5] ? undefined : m[5] === 'Zx' ? 'x' : parseFloat(m[5].slice(1)), 'detailsym': !m[6] ? undefined : m[6].match(new RegExp(re$3.detailsym, 'g')).map(val => { const parts = val.split('_'); const detail = parts[1].split(',').map(prefixColor$2); return { 'index': parseInt(parts[0].slice(1)), 'detail': detail }; }), 'classes': !m[7] ? undefined : m[7], 'id': !m[8] ? undefined : m[8] }); }; /** The convert module contains functions to convert between Formal SignWriitng in ASCII (FSW) and SignWriting in Unicode (SWU) characters, along with other types of data. * [Characters set definitions](https://tools.ietf.org/id/draft-slevinski-formal-signwriting-09.html#name-characters) * @module convert */ /** * Function to convert an FSW coordinate string to an array of x,y integers * @function convert.fsw2coord * @param {string} fswCoord - An FSW coordinate string * @returns {number[]} Array of x,y integers * @example * convert.fsw2coord('500x500') * * return [500, 500] */ const fsw2coord = fswCoord => fswCoord.split('x').map(num => parseInt(num)); const parse$3 = { /** * Function to parse an fsw symbol with optional coordinate and style string * @function fsw.parse.symbol * @param {string} fswSym - an fsw symbol * @returns {SymbolObject} elements of fsw symbol * @example * fsw.parse.symbol('S10000500x500-C') * * return { * 'symbol': 'S10000', * 'coord': [500, 500], * 'style': '-C' * } */ symbol: fswSym => { const regex = `^(${re$1$1.symbol})(${re$1$1.coord})?(${re$3.full})?`; const symbol = typeof fswSym === 'string' ? fswSym.match(new RegExp(regex)) : undefined; return { 'symbol': symbol ? symbol[1] : undefined, 'coord': symbol && symbol[2] ? fsw2coord(symbol[2]) : undefined, 'style': symbol ? symbol[3] : undefined }; }, /** * Function to parse an fsw sign with style string * @function fsw.parse.sign * @param {string} fswSign - an fsw sign * @returns { SignObject } elements of fsw sign * @example * fsw.parse.sign('AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475-C') * * return { * sequence: ['S10011', 'S10019', 'S2e704', 'S2e748'], * box: 'M', * max: [525, 535], * spatials: [ * { * symbol: 'S2e748', * coord: [483, 510] * }, * { * symbol: 'S10011', * coord: [501, 466] * }, * { * symbol: 'S2e704', * coord: [510, 500] * }, * { * symbol: 'S10019', * coord: [476, 475] * } * ], * style: '-C' * } */ sign: fswSign => { const regex = `^(${re$1$1.prefix})?(${re$1$1.signbox})(${re$3.full})?`; const sign = typeof fswSign === 'string' ? fswSign.match(new RegExp(regex)) : undefined; if (sign) { return { 'sequence': sign[1] ? sign[1].slice(1).match(/.{6}/g) : undefined, 'box': sign[2][0], 'max': fsw2coord(sign[2].slice(1, 8)), 'spatials': sign[2].length < 9 ? undefined : sign[2].slice(8).match(/(.{13})/g).map(m => { return { symbol: m.slice(0, 6), coord: [parseInt(m.slice(6, 9)), parseInt(m.slice(10, 13))] }; }), 'style': sign[3] }; } else { return {}; } }, /** * Function to parse an fsw text * @function fsw.parse.text * @param {string} fswText - an fsw text * @returns {string[]} fsw signs and punctuations * @example * fsw.parse.text('AS14c20S27106M518x529S14c20481x471S27106503x489 AS18701S1870aS2e734S20500M518x533S1870a489x515S18701482x490S20500508x496S2e734500x468 S38800464x496') * * return [ * 'AS14c20S27106M518x529S14c20481x471S27106503x489', * 'AS18701S1870aS2e734S20500M518x533S1870a489x515S18701482x490S20500508x496S2e734500x468', * 'S38800464x496' * ] */ text: fswText => { if (typeof fswText !== 'string') return []; const regex = `(${re$1$1.sign}(${re$3.full})?|${re$1$1.spatial}(${re$3.full})?)`; const matches = fswText.match(new RegExp(regex, 'g')); return matches ? [...matches] : []; } }; const compose$2 = { /** * Function to compose an fsw symbol with optional coordinate and style string * @function fsw.compose.symbol * @param {SymbolObject} fswSymObject - an fsw symbol object * @returns {string} an fsw symbol string * @example * fsw.compose.symbol({ * 'symbol': 'S10000', * 'coord': [480, 480], * 'style': '-C' * }) * * return 'S10000480x480-C' */ symbol: fswSymObject => { if (typeof fswSymObject.symbol === 'string') { const symbol = (fswSymObject.symbol.match(re$1$1.symbol) || [''])[0]; if (symbol) { const x = (fswSymObject.coord && fswSymObject.coord[0] || '').toString(); const y = (fswSymObject.coord && fswSymObject.coord[1] || '').toString(); const coord = ((x + 'x' + y).match(re$1$1.coord) || [''])[0] || ''; const styleStr = typeof fswSymObject.style === 'string' && (fswSymObject.style.match(re$3.full) || [''])[0] || ''; return symbol + coord + styleStr; } } return undefined; }, /** * Function to compose an fsw sign with style string * @function fsw.compose.sign * @param {SignObject} fswSignObject - an fsw symbol object * @returns {string} an fsw sign string * @example * fsw.compose.sign({ * sequence: ['S10011', 'S10019', 'S2e704', 'S2e748'], * box: 'M', * max: [525, 535], * spatials: [ * { * symbol: 'S2e748', * coord: [483, 510] * }, * { * symbol: 'S10011', * coord: [501, 466] * }, * { * symbol: 'S2e704', * coord: [510, 500] * }, * { * symbol: 'S10019', * coord: [476, 475] * } * ], * style: '-C' * }) * * return 'AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475-C' */ sign: fswSignObject => { let box = typeof fswSignObject.box !== 'string' ? 'M' : (fswSignObject.box + 'M').match(re$1$1.box); const x = (fswSignObject.max && fswSignObject.max[0] || '').toString(); const y = (fswSignObject.max && fswSignObject.max[1] || '').toString(); const max = ((x + 'x' + y).match(re$1$1.coord) || [''])[0] || ''; if (!max) return undefined; let prefix = ''; if (fswSignObject.sequence && Array.isArray(fswSignObject.sequence)) { prefix = fswSignObject.sequence.map(key => (key.match(re$1$1.nullorsymbol) || [''])[0]).join(''); prefix = prefix ? 'A' + prefix : ''; } let signbox = ''; if (fswSignObject.spatials && Array.isArray(fswSignObject.spatials)) { signbox = fswSignObject.spatials.map(spatial => { if (typeof spatial.symbol === 'string') { const symbol = (spatial.symbol.match(re$1$1.symbol) || [''])[0]; if (symbol) { const x = (spatial.coord && spatial.coord[0] || '').toString(); const y = (spatial.coord && spatial.coord[1] || '').toString(); const coord = ((x + 'x' + y).match(re$1$1.coord) || [''])[0] || ''; if (coord) { return symbol + coord; } } } return ''; }).join(''); } const styleStr = typeof fswSignObject.style === 'string' && (fswSignObject.style.match(re$3.full) || [''])[0] || ''; return prefix + box + max + signbox + styleStr; } }; /** * Function to gather sizing information about an fsw sign or symbol * @function fsw.info * @param {string} fsw - an fsw sign or symbol * @returns {SegmentInfo} information about the fsw string * @example * fsw.info('AS14c20S27106L518x529S14c20481x471S27106503x489-P10Z2') * * return { * minX: 481, * minY: 471, * width: 37, * height: 58, * lane: -1, * padding: 10, * segment: 'sign', * zoom: 2 * } */ const info$1 = fsw => { let lanes = { "B": 0, "L": -1, "M": 0, "R": 1 }; let parsed = parse$3.sign(fsw); let width, height, segment, x1, x2, y1, y2, lane; if (parsed.spatials) { x1 = Math.min(...parsed.spatials.map(spatial => spatial.coord[0])); x2 = parsed.max[0]; width = x2 - x1; y1 = Math.min(...parsed.spatials.map(spatial => spatial.coord[1])); y2 = parsed.max[1]; height = y2 - y1; segment = 'sign'; lane = parsed.box; } else { parsed = parse$3.symbol(fsw); lane = "M"; if (parsed.coord) { x1 = parsed.coord[0]; width = (500 - x1) * 2; y1 = parsed.coord[1]; height = (500 - y1) * 2; segment = 'symbol'; } else { x1 = 490; width = 20; y1 = 490; height = 20; segment = 'none'; } } let style = parse$1$1(parsed.style); let zoom = style.zoom || 1; let padding = style.padding || 0; return { minX: x1, minY: y1, width: width, height: height, segment: segment, lane: lanes[lane], padding: padding, zoom: zoom }; }; const columnDefaults$1 = { 'height': 500, 'width': 150, 'offset': 50, 'pad': 20, 'margin': 5, 'dynamic': false, 'background': undefined, 'punctuation': { 'spacing': true, 'pad': 30, 'pull': true }, 'style': { 'detail': ['black', 'white'], 'zoom': 1 } }; /** * Function to an object of column options with default values * * @function fsw.columnDefaultsMerge * @param {ColumnOptions} options - object of column options * @returns {ColumnOptions} object of column options merged with column defaults * @example * fsw.columnDefaultsMerge({height: 500,width:150}) * * return { * "height": 500, * "width": 150, * "offset": 50, * "pad": 20, * "margin": 5, * "dynamic": false, * "punctuation": { * "spacing": true, * "pad": 30, * "pull": true * }, * "style": { * "detail": [ * "black", * "white" * ], * "zoom": 1 * } * } */ const columnDefaultsMerge$1 = options => { if (typeof options !== 'object') options = {}; return { ...columnDefaults$1, ...options, punctuation: { ...columnDefaults$1.punctuation, ...options.punctuation }, style: { ...columnDefaults$1.style, ...options.style } }; }; /** * Function to transform an FSW text to an array of columns * * @function fsw.columns * @param {string} fswText - FSW text of signs and punctuation * @param {ColumnOptions} options - object of column options * @returns {{options:ColumnOptions,widths:number[],columns:ColumnData}} object of column options, widths array, and column data * @example * fsw.columns('AS14c20S27106M518x529S14c20481x471S27106503x489 AS18701S1870aS2e734S20500M518x533S1870a489x515S18701482x490S20500508x496S2e734500x468 S38800464x496', {height: 500,width:150}) * * return { * "options": { * "height": 500, * "width": 150, * "offset": 50, * "pad": 20, * "margin": 5, * "dynamic": false, * "punctuation": { * "spacing": true, * "pad": 30, * "pull": true * }, * "style": { * "detail": [ * "black", * "white" * ], * "zoom": 1 * } * }, * "widths": [ * 150 * ], * "columns": [ * [ * { * "x": 56, * "y": 20, * "minX": 481, * "minY": 471, * "width": 37, * "height": 58, * "lane": 0, * "padding": 0, * "segment": "sign", * "text": "AS14c20S27106M518x529S14c20481x471S27106503x489", * "zoom": 1 * }, * { * "x": 57, * "y": 118, * "minX": 482, * "minY": 468, * "width": 36, * "height": 65, * "lane": 0, * "padding": 0, * "segment": "sign", * "text": "AS18701S1870aS2e734S20500M518x533S1870a489x515S18701482x490S20500508x496S2e734500x468", * "zoom": 1 * }, * { * "x": 39, * "y": 203, * "minX": 464, * "minY": 496, * "width": 72, * "height": 8, * "lane": 0, * "padding": 0, * "segment": "symbol", * "text": "S38800464x496", * "zoom": 1 * } * ] * ] * } */ const columns$1 = (fswText, options) => { if (typeof fswText !== 'string') return {}; const values = columnDefaultsMerge$1(options); let input = parse$3.text(fswText); let cursor = 0; let cols = []; let col = []; let plus = 0; let center = parseInt(values.width / 2); let maxHeight = values.height - values.margin; let pullable = true; let finalize = false; for (let val of input) { let informed = info$1(val); cursor += plus; if (values.punctuation.spacing) { cursor += informed.segment == 'sign' ? values.pad : 0; } else { cursor += values.pad; } finalize = cursor + informed.height > maxHeight; if (finalize && informed.segment == 'symbol' && values.punctuation.pull && pullable) { finalize = false; pullable = false; } if (col.length == 0) { finalize = false; } if (finalize) { cursor = values.pad; cols.push(col); col = []; pullable = true; } col.push(Object.assign(informed, { x: center + values.offset * informed.lane - (500 - informed.minX) * informed.zoom * values.style.zoom, y: cursor, text: val })); cursor += informed.height * informed.zoom * values.style.zoom; if (values.punctuation.spacing) { plus = informed.segment == 'sign' ? values.pad : values.punctuation.pad; } else { plus = values.pad; } } if (col.length) { cols.push(col); } // over height issue when pulling punctuation if (values.punctuation.pull) { for (let col of cols) { let last = col[col.length - 1]; let diff = last.y + last.height - (values.height - values.margin); if (diff > 0) { let adj = parseInt(diff / col.length) + 1; for (let i in col) { col[i].y -= adj * i + adj; } } } } // contract, expand, adjust let widths = []; for (let col of cols) { let min = [center - values.offset - values.pad]; let max = [center + values.offset + values.pad]; for (let item of col) { min.push(item.x - values.pad); max.push(item.x + item.width + values.pad); } min = Math.min(...min); max = Math.max(...max); let width = values.width; let adj = 0; if (!values.dynamic) { adj = center - parseInt((min + max) / 2); } else { width = max - min; adj = -min; } for (let item of col) { item.x += adj; } widths.push(width); } return { 'options': values, 'widths': widths, 'columns': cols }; }; /** * Array of numbers for categories of symbols: hand, movement, dynamics, head, trunk & limb, location, and punctuation. * @alias fsw.category * @type {number[]} */ const category$1 = [0x100, 0x205, 0x2f7, 0x2ff, 0x36d, 0x37f, 0x387]; /** * Object of symbol ranges with starting and ending numbers. * * { all, writing, hand, movement, dynamic, head, hcenter, vcenter, trunk, limb, location, punctuation } * @alias fsw.ranges * @type {object} */ const ranges$1 = { 'all': [0x100, 0x38b], 'writing': [0x100, 0x37e], 'hand': [0x100, 0x204], 'movement': [0x205, 0x2f6], 'dynamic': [0x2f7, 0x2fe], 'head': [0x2ff, 0x36c], 'hcenter': [0x2ff, 0x36c], 'vcenter': [0x2ff, 0x375], 'trunk': [0x36d, 0x375], 'limb': [0x376, 0x37e], 'location': [0x37f, 0x386], 'punctuation': [0x387, 0x38b] }; /** * Array of colors associated with the seven symbol categories. * @alias fsw.colors * @type {string[]} */ const colors$1 = ['#0000CC', '#CC0000', '#FF0099', '#006600', '#000000', '#884411', '#FF9900']; /** * Function that returns the standardized color for a symbol. * @function fsw.colorize * @param {string} key - an FSW symbol key * @returns {string} name of standardized color for symbol * @example * fsw.colorize('S10000') * * return '#0000CC' */ const colorize$1 = key => { const parsed = parse$3.symbol(key); let color = '#000000'; if (parsed.symbol) { const dec = parseInt(parsed.symbol.slice(1, 4), 16); const index = category$1.findIndex(val => val > dec); color = colors$1[index < 0 ? 6 : index - 1]; } return color; }; /* support ongoing development */ /* https://patreon.com/signwriting */ /* https://donate.sutton-signwriting.io */ /** * Function that returns the size of a symbol using an FSW symbol key * @function fsw.symbolSize * @param {string} fsw - an FSW symbol key * @returns {number[]} width and height of symbol * @example * fsw.symbolSize("S10000") * * return [15,30] */ const symbolSize$1 = function (fsw) { const parsed = parse$3.symbol(fsw); if (!parsed.symbol) { return undefined; } return symbolSize$2(key2id(fsw)); }; /** * Function that returns a plane 15 character for a symbol line using an FSW symbol key * @function fsw.symbolLine * @param {string} fsw - an FSW symbol key * @returns {string} character for symbol line * @example * fsw.symbolLine('S10000') * * return '󰀁' */ const symbolLine$1 = function (fsw) { return symbolLine$2(key2id(fsw)); }; /** * Function that returns a plane 16 character for a symbol fill using an FSW symbol key * @function fsw.symbolFill * @param {string} fsw - an FSW symbol key * @returns {string} character for symbol fill * @example * font.symbolFill('S10000') * * return '􀀁' */ const symbolFill$1 = function (fsw) { return symbolFill$2(key2id(fsw)); }; /** * Function that creates two text elements for a symbol using an FSW symbol key * @function fsw.symbolText * @param {string} fsw - an FSW symbol key * @returns {string} svg segment for line and fill * @example * fsw.symbolText('S10000') * * return ` <text class="sym-fill" fill="white" style="pointer-events:none;font-family:'SuttonSignWritingFill';font-size:30px;">􀀁</text> * <text class="sym-line" fill="black" style="pointer-events:none;font-family:'SuttonSignWritingLine';font-size:30px;">󰀁</text>` */ const symbolText$1 = function (fsw) { return symbolText$2(key2id(fsw)); }; /** * Sutton SignWriting Core Module v2.0.0 (https://github.com/sutton-signwriting/core) * Author: Steve Slevinski (https://SteveSlevinski.me) * style.mjs is released under the MIT License. */ /** * Object of regular expressions for style strings * * @alias style.re * @type {object} * @property {string} colorize - regular expression for colorize section * @property {string} colorhex - regular expression for color hex values with 3 or 6 characters * @property {string} colorname - regular expression for css color name * @property {string} padding - regular expression for padding section * @property {string} zoom - regular expression for zoom section * @property {string} classbase - regular expression for class name definition * @property {string} id - regular expression for id definition * @property {string} colorbase - regular expression for color hex or color name * @property {string} color - regular expression for single color entry * @property {string} colors - regular expression for double color entry * @property {string} background - regular expression for background section * @property {string} detail - regular expression for color details for line and optional fill * @property {string} detailsym - regular expression for color details for individual symbols * @property {string} classes - regular expression for one or more class names * @property {string} full - full regular expression for style string */ let re$2 = { 'colorize': 'C', 'colorhex': '(?:[0-9a-fA-F]{3}){1,2}', 'colorname': '[a-zA-Z]+', 'padding': 'P[0-9]{2}', 'zoom': 'Z(?:[0-9]+(?:\\.[0-9]+)?|x)', 'classbase': '-?[_a-zA-Z][_a-zA-Z0-9-]{0,100}', 'id': '[a-zA-Z][_a-zA-Z0-9-]{0,100}' }; re$2.colorbase = `(?:${re$2.colorhex}|${re$2.colorname})`; re$2.color = `_${re$2.colorbase}_`; re$2.colors = `_${re$2.colorbase}(?:,${re$2.colorbase})?_`; re$2.background = `G${re$2.color}`; re$2.detail = `D${re$2.colors}`; re$2.detailsym = `D[0-9]{2}${re$2.colors}`; re$2.classes = `${re$2.classbase}(?: ${re$2.classbase})*`; re$2.full = `-(${re$2.colorize})?(${re$2.padding})?(${re$2.background})?(${re$2.detail})?(${re$2.zoom})?(?:-((?:${re$2.detailsym})*))?(?:-(${re$2.classes})?!(?:(${re$2.id})!)?)?`; const prefixColor$1 = color => { const regex = new RegExp(`^${re$2.colorhex}$`); return (regex.test(color) ? '#' : '') + color; }; const definedProps$1 = obj => Object.fromEntries(Object.entries(obj).filter(([k, v]) => v !== undefined)); /** * Function to parse style string to object * @function style.parse * @param {string} styleString - a style string * @returns {StyleObject} elements of style string * @example * style.parse('-CP10G_blue_D_red,Cyan_') * * return { * 'colorize': true, * 'padding': 10, * 'background': 'blue', * 'detail': ['red', 'Cyan'] * } */ const parse$2 = styleString => { const regex = `^${re$2.full}`; const m = (typeof styleString === 'string' ? styleString.match(new RegExp(regex)) : []) || []; return definedProps$1({ 'colorize': !m[1] ? undefined : !!m[1], 'padding': !m[2] ? undefined : parseInt(m[2].slice(1)), 'background': !m[3] ? undefined : prefixColor$1(m[3].slice(2, -1)), 'detail': !m[4] ? undefined : m[4].slice(2, -1).split(',').map(prefixColor$1), 'zoom': !m[5] ? undefined : m[5] === 'Zx' ? 'x' : parseFloat(m[5].slice(1)), 'detailsym': !m[6] ? undefined : m[6].match(new RegExp(re$2.detailsym, 'g')).map(val => { const parts = val.split('_'); const detail = parts[1].split(',').map(prefixColor$1); return { 'index': parseInt(parts[0].slice(1)), 'detail': detail }; }), 'classes': !m[7] ? undefined : m[7], 'id': !m[8] ? undefined : m[8] }); }; /** * Function to compose style string from object * @function style.compose * @param {StyleObject} styleObject - an object of style options * @returns {string} style string * @example * style.compose({ * 'colorize': true, * 'padding': 10, * 'background': 'blue', * 'detail': ['red', 'Cyan'], * 'zoom': 1.1, * 'detailsym': [ * { * 'index': 1, * 'detail': ['#ff00ff'] * }, * { * 'index': 2, * 'detail': ['yellow', 'green'] * } * ], * 'classes': 'primary blinking', * 'id': 'cursor' * }) * * return '-CP10G_blue_D_red,Cyan_Z1.1-D01_ff00ff_D02_yellow,green_-primary blinking!cursor!' */ const compose$1 = styleObject => { if (typeof styleObject !== 'object' || styleObject === null) return undefined; // three sections let style1 = '-'; style1 += !styleObject.colorize ? '' : 'C'; const padding = parseInt(styleObject.padding); style1 += !padding || padding <= 0 || padding > 99 ? '' : 'P' + (padding > 9 ? padding : '0' + padding); const background = !styleObject.background || !(typeof styleObject.background === 'string') ? undefined : styleObject.background.match(re$2.colorbase)[0]; style1 += !background ? '' : 'G_' + background + '_'; const detail1 = !styleObject.detail || !styleObject.detail[0] || !(typeof styleObject.detail[0] === 'string') ? undefined : styleObject.detail[0].match(re$2.colorbase)[0]; const detail2 = !styleObject.detail || !styleObject.detail[1] || !(typeof styleObject.detail[1] === 'string') ? undefined : styleObject.detail[1].match(re$2.colorbase)[0]; if (detail1) { style1 += 'D_' + detail1; if (detail2) { style1 += ',' + detail2; } style1 += '_'; } const zoom = styleObject.zoom === 'x' ? 'x' : parseFloat(styleObject.zoom); style1 += !zoom || zoom <= 0 ? '' : 'Z' + zoom; let style2 = ''; const detailsym = !styleObject.detailsym || !Array.isArray(styleObject.detailsym) ? [] : styleObject.detailsym.map(styleObject => { const index = parseInt(styleObject.index); if (!index || index <= 0 || index > 99) return ''; let style = 'D' + (index > 9 ? index : '0' + index); const detail1 = !styleObject.detail || !styleObject.detail[0] ? undefined : styleObject.detail[0].match(re$2.colorbase)[0]; const detail2 = !styleObject.detail || !styleObject.detail[1] ? undefined : styleObject.detail[1].match(re$2.colorbase)[0]; if (detail1) { style += '_' + detail1; if (detail2) { style += ',' + detail2; } style += '_'; } return style; }); style2 += detailsym.join(''); let style3 = ''; const classes = !styleObject.classes || !(typeof styleObject.classes === 'string') ? undefined : styleObject.classes.match(re$2.classes)[0]; style3 += !classes ? '' : classes; const id = !styleObject.id || !(typeof styleObject.id === 'string') ? undefined : styleObject.id.match(re$2.id)[0]; style3 += classes || id ? '!' : ''; style3 += !id ? '' : id + '!'; return style1 + (style2 || style3 ? '-' + style2 : '') + (style3 ? '-' + style3 : ''); }; /* support ongoing development */ /* https://patreon.com/signwriting */ /* https://donate.sutton-signwriting.io */ /** * Function that creates an SVG image from an FSW symbol key with an optional style string * @function fsw.symbolSvgBody * @param {string} fswSym - an FSW symbol key with optional style string * @returns {string} body of SVG for symbol * @example * fsw.symbolSvgBody('S10000') * * return `<text font-size="0">S10000</text> * <g transform="translate(500,500)"> * <text class="sym-fill" fill="white" style="pointer-events:none;font-family:'SuttonSignWritingFill';font-size:30px;">􀀁</text> * <text class="sym-line" fill="black" style="pointer-events:none;font-family:'SuttonSignWritingLine';font-size:30px;">󰀁</text> * </g>` */ const symbolSvgBody$1 = fswSym => { const parsed = parse$3.symbol(fswSym); const blank = ''; if (!parsed.symbol) return blank; let styling = parse$2(parsed.style); let x1, y1, x2, y2; if (parsed.coord) { x1 = parsed.coord[0]; y1 = parsed.coord[1]; x2 = 500 + (500 - x1); y2 = 500 + (500 - y1); } else { let size = symbolSize$1(parsed.symbol); if (!size) return blank; x1 = 500 - parseInt((size[0] + 1) / 2); y1 = 500 - parseInt((size[1] + 1) / 2); x2 = 500 + (500 - x1); y2 = 500 + (500 - y1); } let symSvg = symbolText$1(parsed.symbol); symSvg = ` <g transform="translate(${x1},${y1})"> ${symSvg} </g>`; let line; if (styling.colorize) { line = colorize$1(parsed.symbol); } else if (styling.detail) { line = styling.detail[0]; } if (line) { symSvg = symSvg.replace(/class="sym-line" fill="black"/, `class="sym-line" fill="${line}"`); } let fill = styling.detail && styling.detail[1]; if (fill) { symSvg = symSvg.replace(/class="sym-fill" fill="white"/, `class="sym-fill" fill="${fill}"`); } let background = ''; if (styling.padding) { x1 -= styling.padding; y1 -= styling.padding; x2 += styling.padding; y2 += styling.padding; } if (styling.background) { background = `\n <rect x="${x1}" y="${y1}" width="${x2 - x1}" height="${y2 - y1}" style="fill:${styling.background};" />`; } return ` <text font-size="0">${fswSym}</text>${background} ${symSvg}`; }; /** * Function that creates an SVG image from an FSW symbol key with an optional style string * @function fsw.symbolSvg * @param {string} fswSym - an FSW symbol key with optional style string * @returns {string} SVG for symbol * @example * fsw.symbolSvg('S10000') * * return `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="15" height="30" viewBox="500 500 15 30"> * <text font-size="0">S10000</text> * <g transform="translate(500,500)"> * <text class="sym-fill" fill="white" style="pointer-events:none;font-family:'SuttonSignWritingFill';font-size:30px;">􀀁</text> * <text class="sym-line" fill="black" style="pointer-events:none;font-family:'SuttonSignWritingLine';font-size:30px;">󰀁</text> * </g> * </svg>` */ const symbolSvg$1 = fswSym => { const parsed = parse$3.symbol(fswSym); const blank = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>'; if (!parsed.symbol) return blank; let styling = parse$2(parsed.style); let x1, y1, x2, y2; if (parsed.coord) { x1 = parsed.coord[0]; y1 = parsed.coord[1]; x2 = 500 + (500 - x1); y2 = 500 + (500 - y1); } else { let size = symbolSize$1(parsed.symbol); if (!size) return blank; x1 = parseInt(500 - size[0] / 2); y1 = parseInt(500 - size[1] / 2); x2 = x1 + size[0]; y2 = y1 + size[1]; } let classes = ''; if (styling.classes) { classes = ` class="${styling.classes}"`; } let id = ''; if (styling.id) { id = ` id="${styling.id}"`; } if (styling.padding) { x1 -= styling.padding; y1 -= styling.padding; x2 += styling.padding; y2 += styling.padding; } let sizing = ''; if (styling.zoom != 'x') { sizing = ` width="${(x2 - x1) * (styling.zoom ? styling.zoom : 1)}" height="${(y2 - y1) * (styling.zoom ? styling.zoom : 1)}"`; } return `<svg${classes}${id} version="1.1" xmlns="http://www.w3.org/2000/svg"${sizing} viewBox="${x1} ${y1} ${x2 - x1} ${y2 - y1}"> ${symbolSvgBody$1(fswSym)} </svg>`; }; const symbolCanvas$1 = function (fswSym) { const parsed = parse$3.symbol(fswSym); if (parsed.symbol) { let size = symbolSize$1(parsed.symbol); if (size) { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); let styling = parse$2(parsed.style); let line = 'black'; if (styling.colorize) { line = colorize$1(parsed.symbol); } else if (styling.detail) { line = styling.detail[0]; } let fill = styling.detail && styling.detail[1] || 'white'; let x1 = 500; let x2 = x1 + size[0]; let y1 = 500; let y2 = y1 + size[1]; if (styling.padding) { x1 -= styling.padding; y1 -= styling.padding; x2 += styling.padding; y2 += styling.padding; } let sizing = 1; if (styling.zoom != 'x') { sizing = styling.zoom; } let w = (x2 - x1) * sizing; let h = (y2 - y1) * sizing; canvas.width = w ? w : 1; canvas.height = h ? h : 1; if (styling.background) { context.rect(0, 0, w, h); context.fillStyle = styling.background; context.fill(); } context.font = 30 * sizing + "px 'SuttonSignWritingFill'"; context.fillStyle = fill; context.fillText(symbolFill$1(parsed.symbol), (500 - x1) * sizing, (500 - y1) * sizing); context.font = 30 * sizing + "px 'SuttonSignWritingLine'"; context.fillStyle = line; context.fillText(symbolLine$1(parsed.symbol), (500 - x1) * sizing, (500 - y1) * sizing); return canvas; } } }; /** * Function that creates a PNG data url from an FSW symbol key with an optional style string * @function fsw.symbolPng * @param {string} fswSym - an FSW symbol key with optional style string * @returns {string} png image for symbol as data url * @example * fsw.symbolPng('S10000') * * return 'data:image/png;base64,iVBORw...' */ const symbolPng$1 = fswSym => { const canvas = symbolCanvas$1(fswSym); const png = canvas.toDataURL("image/png"); canvas.remove(); return png; }; const blank$1 = null; /** * Function that normalizes a symbol with a minimum coordinate for a center of 500,500 * @function fsw.symbolNormalize * @param {string} fswSym - an FSW symbol key with optional coordinate and style string * @returns {string} normalized FSW symbol * @example * fsw.symbolNormalize('S10000-CP10G_green_Z2') * * return 'S10000493x485-CP10G_green_Z2' */ const symbolNormalize$1 = fswSym => { const parsed = parse$3.symbol(fswSym); if (parsed.symbol) { let size = symbolSize$1(parsed.symbol); if (size) { return `${parsed.symbol}${500 - parseInt((size[0] + 1) / 2)}x${500 - parseInt((size[1] + 1) / 2)}${parsed.style || ''}`; } } else { return blank$1; } }; /** * Function that mirrors a symbol * @function fsw.symbolMirror * @param {string} fswSym - an FSW symbol key with optional coordinate and style string * @returns {string} mirrored FSW symbol * @example * fsw.symbolMirror('S10000') * * return 'S10008' */ const symbolMirror$1 = fswSym => { let parsed = parse$3.symbol(fswSym); if (!p