@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,509 lines (1,451 loc) âĒ 84.4 kB
JavaScript
/**
* Sutton SignWriting TrueType Font Module v1.6.0 (https://github.com/sutton-signwriting/font-ttf)
* Author: Steve Slevinski (https://SteveSlevinski.me)
* swu.js is released under the MIT License.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.ssw = global.ssw || {}, global.ssw.ttf = global.ssw.ttf || {}, global.ssw.ttf.swu = {})));
})(this, (function (exports) { 'use strict';
/**
* 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 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)
* swu.mjs is released under the MIT License.
*/
/**
* Object of regular expressions for SWU strings in UTF-16
*
* @alias swu.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 = {
'null': '\uD8C0\uDC00',
'symbol': '(?:(?:\uD8C0[\uDC01-\uDFFF])|(?:[\uD8C1-\uD8FC][\uDC00-\uDFFF])|(?:\uD8FD[\uDC00-\uDC80]))',
'coord': '(?:\uD836[\uDC0C-\uDDFF]){2}',
'sort': '\uD836\uDC00',
'box': '\uD836[\uDC01-\uDC04]'
};
re$1.nullorsymbol = `(?:${re$1.null}|${re$1.symbol})`;
re$1.prefix = `(?:${re$1.sort}(?:${re$1.nullorsymbol})+)`;
re$1.spatial = `${re$1.symbol}${re$1.coord}`;
re$1.signbox = `${re$1.box}${re$1.coord}(?:${re$1.spatial})*`;
re$1.sign = `${re$1.prefix}?${re$1.signbox}`;
re$1.sortable = `${re$1.prefix}${re$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$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$1 = 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]
});
};
/** 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 SWU number character to an integer
* @function convert.swu2num
* @param {string} swuNum - SWU number character
* @returns {number} Integer value for number
* @example
* convert.swu2num('ðĪ')
*
* return 500
*/
const swu2num = swuNum => parseInt(swuNum.codePointAt(0)) - 0x1D80C + 250;
/**
* 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 = num => String.fromCodePoint(0x1D80C + parseInt(num) - 250);
/**
* Function to convert two SWU number characters to an array of x,y integers
* @function convert.swu2coord
* @param {string} swuCoord - Two SWU number character
* @returns {number[]} Array of x,y integers
* @example
* convert.swu2coord('ðĪðĪ')
*
* return [500, 500]
*/
const swu2coord = swuCoord => [swu2num(swuCoord.slice(0, 2)), swu2num(swuCoord.slice(2, 4))];
/**
* 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 = coord => coord.map(num => num2swu(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 = swuSym => parseInt(swuSym.codePointAt(0));
const parse$2 = {
/**
* Function to parse an swu symbol with optional coordinate and style string
* @function swu.parse.symbol
* @param {string} swuSym - an swu symbol
* @returns {SymbolObject} elements of swu symbol
* @example
* swu.parse.symbol('ņðĪðĪ-C')
*
* return {
* 'symbol': 'ņ',
* 'coord': [500, 500],
* 'style': '-C'
* }
*/
symbol: swuSym => {
const regex = `^(${re$1.symbol})(${re$1.coord})?(${re$2.full})?`;
const symbol = typeof swuSym === 'string' ? swuSym.match(new RegExp(regex)) : undefined;
return {
'symbol': symbol ? symbol[1] : undefined,
'coord': symbol && symbol[2] ? swu2coord(symbol[2]) : undefined,
'style': symbol ? symbol[3] : undefined
};
},
/**
* Function to parse an swu sign with style string
* @function swu.parse.sign
* @param {string} swuSign - an swu sign
* @returns {SignObject} elements of swu sign
* @example
* swu.parse.sign('ð ņņņĨņĐð ðĪðĪĐņĐðĢĩðĪņðĪðĢĪņĨðĪðĪņðĢŪðĢ-C')
*
* return {
* sequence: ['ņ','ņ','ņĨ','ņĐ'],
* box: 'ð ',
* max: [525, 535],
* spatials: [
* {
* symbol: 'ņĐ',
* coord: [483, 510]
* },
* {
* symbol: 'ņ',
* coord: [501, 466]
* },
* {
* symbol: 'ņĨ',
* coord: [510, 500]
* },
* {
* symbol: 'ņ',
* coord: [476, 475]
* }
* ],
* style: '-C'
* }
*/
sign: swuSign => {
const regex = `^(${re$1.prefix})?(${re$1.signbox})(${re$2.full})?`;
const sign = typeof swuSign === 'string' ? swuSign.match(new RegExp(regex)) : undefined;
if (sign) {
return {
'sequence': sign[1] ? sign[1].slice(2).match(/.{2}/g) : undefined,
'box': sign[2].slice(0, 2),
'max': swu2coord(sign[2].slice(2, 6)),
'spatials': sign[2].length < 7 ? undefined : sign[2].slice(6).match(/(.{6})/g).map(m => {
return {
symbol: m.slice(0, 2),
coord: swu2coord(m.slice(2))
};
}),
'style': sign[3]
};
} else {
return {};
}
},
/**
* Function to parse an swu text
* @function swu.parse.text
* @param {string} swuText - an swu text
* @returns {string[]} swu signs and punctuations
* @example
* swu.parse.text('ð ņēĄņЧð ðĪðĪĢņēĄðĢģðĢĐņЧðĪðĢŧ ð ņĒņŦņņĄð ðĪðΧņŦðĢŧðĪņĒðĢīðĢžņĄðĪðĪņðĪðĢĶ ņðĢĒðĪ')
*
* return [
* 'ð ņēĄņЧð ðĪðĪĢņēĄðĢģðĢĐņЧðĪðĢŧ',
* 'ð ņĒņŦņņĄð ðĪðΧņŦðĢŧðĪņĒðĢīðĢžņĄðĪðĪņðĪðĢĶ',
* 'ņðĢĒðĪ'
* ]
*/
text: swuText => {
if (typeof swuText !== 'string') return [];
const regex = `(${re$1.sign}(${re$2.full})?|${re$1.spatial}(${re$2.full})?)`;
const matches = swuText.match(new RegExp(regex, 'g'));
return matches ? [...matches] : [];
}
};
const compose$1 = {
/**
* Function to compose an swu symbol with optional coordinate and style string
* @function swu.compose.symbol
* @param {SymbolObject} swuSymObject - an swu symbol object
* @returns {string} an swu symbol string
* @example
* swu.compose.symbol({
* 'symbol': 'ņ',
* 'coord': [500, 500],
* 'style': '-C'
* })
*
* return 'ņðĪðĪ-C'
*/
symbol: swuSymObject => {
if (typeof swuSymObject !== 'object' || swuSymObject === null) return undefined;
if (typeof swuSymObject.symbol === 'string') {
const symbol = (swuSymObject.symbol.match(re$1.symbol) || [''])[0];
if (symbol) {
const x = swuSymObject.coord && swuSymObject.coord[0] || '';
const y = swuSymObject.coord && swuSymObject.coord[1] || '';
const coord = x && y ? coord2swu([x, y]) : '';
const styleStr = typeof swuSymObject.style === 'string' && (swuSymObject.style.match(re$2.full) || [''])[0] || '';
return symbol + coord + styleStr;
}
}
return undefined;
},
/**
* Function to compose an swu sign with style string
* @function swu.compose.sign
* @param {SignObject} swuSignObject - an swu sign object
* @returns {string} an swu sign string
* @example
* swu.compose.sign({
* sequence: ['ņ','ņ','ņĨ','ņĐ'],
* box: 'ð ',
* max: [525, 535],
* spatials: [
* {
* symbol: 'ņĐ',
* coord: [483, 510]
* },
* {
* symbol: 'ņ',
* coord: [501, 466]
* },
* {
* symbol: 'ņĨ',
* coord: [510, 500]
* },
* {
* symbol: 'ņ',
* coord: [476, 475]
* }
* ],
* style: '-C'
* })
*
* return 'ð ņņņĨņĐð ðĪðĪĐņĐðĢĩðĪņðĪðĢĪņĨðĪðĪņðĢŪðĢ-C'
*/
sign: swuSignObject => {
if (typeof swuSignObject !== 'object' || swuSignObject === null) return undefined;
let box = typeof swuSignObject.box !== 'string' ? 'ð ' : (swuSignObject.box + 'ð ').match(re$1.box);
const x = swuSignObject.max && swuSignObject.max[0] || '';
const y = swuSignObject.max && swuSignObject.max[1] || '';
const max = x && y ? coord2swu([x, y]) : undefined;
if (!max) return undefined;
let prefix = '';
if (swuSignObject.sequence && Array.isArray(swuSignObject.sequence)) {
prefix = swuSignObject.sequence.map(key => (key.match(re$1.nullorsymbol) || [''])[0]).join('');
prefix = prefix ? 'ð ' + prefix : '';
}
let signbox = '';
if (swuSignObject.spatials && Array.isArray(swuSignObject.spatials)) {
signbox = swuSignObject.spatials.map(spatial => {
if (typeof spatial.symbol === 'string') {
const symbol = (spatial.symbol.match(re$1.symbol) || [''])[0];
if (symbol) {
const x = spatial.coord && spatial.coord[0] || '';
const y = spatial.coord && spatial.coord[1] || '';
const coord = x && y ? coord2swu([x, y]) : '';
if (coord) {
return symbol + coord;
}
}
}
return '';
}).join('');
}
const styleStr = typeof swuSignObject.style === 'string' && (swuSignObject.style.match(re$2.full) || [''])[0] || '';
return prefix + box + max + signbox + styleStr;
}
};
/**
* Function to gather sizing information about an swu sign or symbol
* @function swu.info
* @param {string} swu - an swu sign or symbol
* @returns {SegmentInfo} information about the swu string
* @example
* swu.info('ð ņēĄņЧð ðĪðĪĢņēĄðĢģðĢĐņЧðĪðĢŧ-P10Z2')
*
* return {
* minX: 481,
* minY: 471,
* width: 37,
* height: 58,
* lane: -1,
* padding: 10,
* segment: 'sign',
* zoom: 2
* }
*/
const info = swu => {
let lanes = {
'ð ': 0,
'ð ': -1,
'ð ': 0,
'ð ': 1
};
let parsed = parse$2.sign(swu);
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$2.symbol(swu);
lane = "ð ";
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(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 = {
'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 swu.columnDefaultsMerge
* @param {ColumnOptions} options - object of column options
* @returns {ColumnOptions} object of column options merged with column defaults
* @example
* swu.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 = options => {
if (typeof options !== 'object') options = {};
return {
...columnDefaults,
...options,
punctuation: {
...columnDefaults.punctuation,
...options.punctuation
},
style: {
...columnDefaults.style,
...options.style
}
};
};
/**
* Function to transform an SWU text to an array of columns
*
* @function swu.columns
* @param {string} swuText - SWU 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
* swu.columns('ð ņēĄņЧð ðĪðĪĢņēĄðĢģðĢĐņЧðĪðĢŧ ð ņĒņŦņņĄð ðĪðΧņŦðĢŧðĪņĒðĢīðĢžņĄðĪðĪņðĪðĢĶ ņðĢĒðĪ', {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": "ð ņēĄņЧð ðĪðĪĢņēĄðĢģðĢĐņЧðĪðĢŧ",
* "zoom": 1
* },
* {
* "x": 57,
* "y": 118,
* "minX": 482,
* "minY": 468,
* "width": 36,
* "height": 65,
* "lane": 0,
* "padding": 0,
* "segment": "sign",
* "text": "ð ņĒņŦņņĄð ðĪðΧņŦðĢŧðĪņĒðĢīðĢžņĄðĪðĪņðĪðĢĶ",
* "zoom": 1
* },
* {
* "x": 39,
* "y": 203,
* "minX": 464,
* "minY": 496,
* "width": 72,
* "height": 8,
* "lane": 0,
* "padding": 0,
* "segment": "symbol",
* "text": "ņðĢĒðĪ",
* "zoom": 1
* }
* ]
* ]
* }
*/
const columns = (swuText, options) => {
if (typeof swuText !== 'string') return {};
const values = columnDefaultsMerge(options);
let input = parse$2.text(swuText);
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(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 plane 4 code points for categories of symbols: hand, movement, dynamics, head, trunk & limb, location, and punctuation.
* @alias swu.category
* @type {array}
*/
const category = [0x40001, 0x461e1, 0x4bca1, 0x4bfa1, 0x4e8e1, 0x4efa1, 0x4f2a1];
/**
* Object of symbol ranges with starting and ending code points on plane 4.
*
* { all, writing, hand, movement, dynamic, head, hcenter, vcenter, trunk, limb, location, punctuation }
* @alias swu.ranges
* @type {object}
*/
const ranges = {
'all': [0x40001, 0x4f480],
'writing': [0x40001, 0x4efa0],
'hand': [0x40001, 0x461e0],
'movement': [0x461e1, 0x4bca0],
'dynamic': [0x4bca1, 0x4bfa0],
'head': [0x4bfa1, 0x4e8e0],
'hcenter': [0x4bfa1, 0x4e8e0],
'vcenter': [0x4bfa1, 0x4ec40],
'trunk': [0x4e8e1, 0x4ec40],
'limb': [0x4ec41, 0x4efa0],
'location': [0x4efa1, 0x4f2a0],
'punctuation': [0x4f2a1, 0x4f480]
};
/**
* Array of colors associated with the seven symbol categories.
* @alias swu.colors
* @type {array}
*/
const colors = ['#0000CC', '#CC0000', '#FF0099', '#006600', '#000000', '#884411', '#FF9900'];
/**
* Function that returns the standardized color for a symbol.
* @function swu.colorize
* @param {string} swuSym - an SWU symbol character
* @returns {string} name of standardized color for symbol
* @example
* swu.colorize('ņ')
*
* return '#0000CC'
*/
const colorize = swuSym => {
const parsed = parse$2.symbol(swuSym);
let color = '#000000';
if (parsed.symbol) {
const code = swu2code(parsed.symbol);
const index = category.findIndex(val => val > code);
color = colors[index < 0 ? 6 : index - 1];
}
return color;
};
/* support ongoing development */
/* https://patreon.com/signwriting */
/* https://donate.sutton-signwriting.io */
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$1 = 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 the size of a symbol using an SWU symbol character
* @function swu.symbolSize
* @param {string} swu - an SWU symbol character
* @returns {number[]} width and height of symbol
* @example
* swu.symbolSize("ņ")
*
* return [15,30]
*/
const symbolSize = function (swu) {
const parsed = parse$2.symbol(swu);
if (!parsed.symbol) {
return undefined;
}
return symbolSize$1(swu2id(swu));
};
/**
* 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$1 = 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$1 = 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$1 = function (id) {
return ` <text class="sym-fill" fill="white" style="pointer-events:none;font-family:'SuttonSignWritingFill';font-size:30px;">${symbolFill$1(id)}</text>
<text class="sym-line" fill="black" style="pointer-events:none;font-family:'SuttonSignWritingLine';font-size:30px;">${symbolLine$1(id)}</text>`;
};
/**
* Function that returns a plane 15 character for a symbol line using an SWU symbol character
* @function swu.symbolLine
* @param {string} swu - an SWU symbol character
* @returns {string} character for symbol line
* @example
* swu.symbolLine('ņ')
*
* return 'ó°'
*/
const symbolLine = function (swu) {
return symbolLine$1(swu2id(swu));
};
/**
* Function that returns a plane 165 character for a symbol fill using an SWU symbol character
* @function swu.symbolFill
* @param {string} swu - an SWU symbol character
* @returns {string} character for symbol fill
* @example
* swu.symbolFill('ņ')
*
* return 'ô'
*/
const symbolFill = function (swu) {
return symbolFill$1(swu2id(swu));
};
/**
* Function that creates two text elements for a symbol using an SWU symbol character
* @function swu.symbolText
* @param {string} swu - an SWU symbol character
* @returns {string} svg segment for line and fill
* @example
* swu.symbolText('ņ')
*
* 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 = function (swu) {
return symbolText$1(swu2id(swu));
};
/**
* 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 = {
'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.colorbase = `(?:${re.colorhex}|${re.colorname})`;
re.color = `_${re.colorbase}_`;
re.colors = `_${re.colorbase}(?:,${re.colorbase})?_`;
re.background = `G${re.color}`;
re.detail = `D${re.colors}`;
re.detailsym = `D[0-9]{2}${re.colors}`;
re.classes = `${re.classbase}(?: ${re.classbase})*`;
re.full = `-(${re.colorize})?(${re.padding})?(${re.background})?(${re.detail})?(${re.zoom})?(?:-((?:${re.detailsym})*))?(?:-(${re.classes})?!(?:(${re.id})!)?)?`;
const prefixColor = color => {
const regex = new RegExp(`^${re.colorhex}$`);
return (regex.test(color) ? '#' : '') + color;
};
const definedProps = 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 = styleString => {
const regex = `^${re.full}`;
const m = (typeof styleString === 'string' ? styleString.match(new RegExp(regex)) : []) || [];
return definedProps({
'colorize': !m[1] ? undefined : !!m[1],
'padding': !m[2] ? undefined : parseInt(m[2].slice(1)),
'background': !m[3] ? undefined : prefixColor(m[3].slice(2, -1)),
'detail': !m[4] ? undefined : m[4].slice(2, -1).split(',').map(prefixColor),
'zoom': !m[5] ? undefined : m[5] === 'Zx' ? 'x' : parseFloat(m[5].slice(1)),
'detailsym': !m[6] ? undefined : m[6].match(new RegExp(re.detailsym, 'g')).map(val => {
const parts = val.split('_');
const detail = parts[1].split(',').map(prefixColor);
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 = 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.colorbase)[0];
style1 += !background ? '' : 'G_' + background + '_';
const detail1 = !styleObject.detail || !styleObject.detail[0] || !(typeof styleObject.detail[0] === 'string') ? undefined : styleObject.detail[0].match(re.colorbase)[0];
const detail2 = !styleObject.detail || !styleObject.detail[1] || !(typeof styleObject.detail[1] === 'string') ? undefined : styleObject.detail[1].match(re.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.colorbase)[0];
const detail2 = !styleObject.detail || !styleObject.detail[1] ? undefined : styleObject.detail[1].match(re.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.classes)[0];
style3 += !classes ? '' : classes;
const id = !styleObject.id || !(typeof styleObject.id === 'string') ? undefined : styleObject.id.match(re.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 SWU symbol key with an optional style string
* @function swu.symbolSvgBody
* @param {string} swuSym - an SWU symbol key with optional style string
* @returns {string} body of SVG for symbol
* @example
* swu.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 = swuSym => {
const parsed = parse$2.symbol(swuSym);
const blank = '';
if (!parsed.symbol) return blank;
let styling = parse(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(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(parsed.symbol);
symSvg = ` <g transform="translate(${x1},${y1})">
${symSvg}
</g>`;
let line;
if (styling.colorize) {
line = colorize(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">${swuSym}</text>${background}
${symSvg}`;
};
/**
* Function that creates an SVG image from an SWU symbol key with an optional style string
* @function swu.symbolSvg
* @param {string} swuSym - an SWU symbol key with optional style string
* @returns {string} SVG for symbol
* @example
* swu.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 = swuSym => {
const parsed = parse$2.symbol(swuSym);
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(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(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(swuSym)}
</svg>`;
};
const symbolCanvas = function (swuSym) {
const parsed = parse$2.symbol(swuSym);
if (parsed.symbol) {
let size = symbolSize(parsed.symbol);
if (size) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
let styling = parse(parsed.style);
let line = 'black';
if (styling.colorize) {
line = colorize(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(parsed.symbol), (500 - x1) * sizing, (500 - y1) * sizing);
context.font = 30 * sizing + "px 'SuttonSignWritingLine'";
context.fillStyle = line;
context.fillText(symbolLine(parsed.symbol), (500 - x1) * sizing, (500 - y1) * sizing);
return canvas;
}
}
};
/**
* Function that creates a PNG data url from an SWU symbol character with an optional style string
* @function swu.symbolPng
* @param {string} swuSym - an SWU symbol character with optional style string
* @returns {string} png image for symbol as data url
* @example
* swu.symbolPng('ņ-CP10G_green_Z2')
*
* return 'data:image/png;base64,iVBORw...'
*/
const symbolPng = swuSym => {
const canvas = symbolCanvas(swuSym);
const png = canvas.toDataURL("image/png");
canvas.remove();
return png;
};
const blank = null;
/**
* Function that normalizes a symbol with a minimum coordinate for a center of 500,500
* @function swu.symbolNormalize
* @param {string} swuSym - an SWU symbol character with optional coordinate and style string
* @returns {string} normalized SWU symbol
* @example
* swu.symbolNormalize('ņ')
*
* return 'ņðĢŋðĢ·'
*/
const symbolNormalize = swuSym => {
const parsed = parse$2.symbol(swuSym);
if (parsed.symbol) {
let size = symbolSize(parsed.symbol);
if (size) {
return `${parsed.symbol}${coord2swu$1([500 - parseInt((size[0] + 1) / 2), 500 - parseInt((size[1] + 1) / 2)])}${parsed.style || ''}`;
}
} else {
return blank;
}
};
/**
* Function that mirrors a symbol
* @function swu.symbolMirror
* @param {string} swuSym - an SWU symbol with optional coordinate and style string
* @returns {string} mirrored SWU symbol
* @example
* swu.symbolMirror('ņ')
*
* return 'ņ'
*/
const symbolMirror = swuSym => {
let parsed = parse$2.symbol(swuSym);
if (!parsed.symbol) {
return swuSym;
}
const size = symbolSize(parsed.symbol);
if (!size) {
return swuSym;
}
parsed.symbol = swu2key(parsed.symbol);
const base = parsed.symbol.slice(0, 4);
let fill = parsed.symbol.slice(4, 5);
let rot = parseInt(parsed.symbol.slice(5, 6), 16);
const key1 = base + "08";
const key2 = base + "18";
var rAdd;
if (symbolSize(key2swu(key1)) || symbolSize(key2swu(key2))) {
rAdd = 8;
} else {
if (rot === 0 || rot == 4) {
rAdd = 0;
}
if (rot == 1 || rot == 5) {
rAdd = 6;
}