import { constructors } from '../core/library.js';
import { mergeOver, xt } from '../core/utilities.js';
import { requestCell, releaseCell } from './cell.js';
import baseMix from '../mixin/base.js';FontAttribute objects are used exclusively by Phrase entitys. They hold data about the Phrase entity’s current font
The Phrase entity includes functionality to allow the getting and setting of FontAttribute attributes directly on the entity instance.
Note that <canvas> context engines will attempt to display variable fonts, but the added functionality of those fonts is, for the most part, ignored. Scrawl-canvas makes no overt attempts to overcome this limitation.
import { constructors } from '../core/library.js';
import { mergeOver, xt } from '../core/utilities.js';
import { requestCell, releaseCell } from './cell.js';
import baseMix from '../mixin/base.js';const FontAttributes = function (items = {}) {
this.makeName(items.name);
this.set(this.defs);
this.set(items);
return this;
};let P = FontAttributes.prototype = Object.create(Object.prototype);
P.type = 'FontAttributes';
P.lib = 'fontattribute';P = baseMix(P);let defaultAttributes = {font - pseudo-attribute String which gets broken down into its component parts.
font-style - saved in the style String attribute - acceptable values are: normal, italic and oblique. Note that browser handling of oblique (sloped rather than explicitly italic) fonts by their respective canvas context engines is, at best, idiosyncratic.
family or font strings. style: 'normal',font-variant - saved in the variant String attribute - the standard indicates that canvas context engine should only recognise normal and small-caps values. Do not use other possibilities (font-variant-caps, font-variant-numeric, font-variant-ligatures, font-variant-east-asian, font-variant-alternates) in font strings; scrawl-canvas will remove and/or ignore them when it parses the font string.
variant: 'normal',font-weight - saved in the weight String attribute - acceptable values are: normal, bold, lighter, bolder; or a number (100. 200, 300, … 900). Bold is generally the equivalent of 700, and normal is 400; lighter/bolder are values relative to the <canvas> element’s computed font weight. Note that browser handling of font weight requirements by their respective canvas context engines is not entirely standards compliant - for instance Safari browsers will generally ignore weight assertions in font strings.
family or font strings. weight: 'normal',font-stretch - saved in the stretch String attribute acceptable values are: normal, semi-condensed, condensed, extra-condensed, ultra-condensed, semi-expanded, expanded, extra-expanded, ultra-expanded. Browser support for these values by the <canvas> element’s context engine is, for the most part, non-existant.
family or font strings. stretch: 'normal',font-size - broken into two parts and saved in the sizeValue Number and sizeMetric String, each of which can be set separately. The W3C HTML Canvas 2D Context Recommendation states: “with the ‘font-size’ component converted to CSS pixels”. However, Scrawl-canvas makes every effort to respect and interpret non-px-based font-size requests.
Length values relative to some intrinsic propertly of the font - Scrawl-canvas will not attempt to interpret the following, instead treating them as some fixed proportion of the <canvas> element’s computed font size:
1em is the <canvas> element’s computed font size1rem is the document root (<html>) element’s computed font size1lh is the <canvas> element’s computed font size multiplied by the entity’s line height value1rlh is the document root (<html>) element’s computed font size multiplied by the entity’s line height value1ex is ≈ ‘0.5em’1cap, 1ch, 1ic are all ≈ ‘1em’Length percentage values, with calculation based on the <canvas> element’s computed font size:
120% = ‘1.2em’Non-numerical values (in the Fonts standards, ‘absolute-size’ and ‘relative-size’ keywords) will calculate a size based on the <canvas> element’s computed font size:
xx-small = 60% of the computed font sizex-small = 75% of the computed font sizesmall = 89% of the computed font sizemedium = 100% of the computed font sizelarge = 120% of the computed font sizex-large = 150% of the computed font sizexx-large = 200% of the computed font sizexxx-large = 300% of the computed font sizesmaller = 80% of the computed font sizelarger = 130% of the computed font sizeLength values relative to the browser’s viewport (window) dimensions:
1vw and 1vh represent 1% of the viewport’s width and height, respectively1vmax - is 1% of the larger viewport dimension1vmin - is 1% of the smaller viewport dimension1vi as ≈ 1vw, and 1vb as ≈ 1vhAbsolute length values convert as follow:
1in (inch) = 96px1cm (centimeter) = 37.80px1mm (millimeter) = 3.78px1Q (quarter mm) = 0.95px 1pc (pica) = 16px1pt (point) = 1.33px1px (pixel) = 1pxNote: we break down the size attribute into two components: sizeValue and sizeMetric. Line height values are ignored and, when present in a font string, may break the code!
sizeValue: 12,
sizeMetric: 'px',font-family - any part of the font string that comes after the above declarations.
serif, sans-serif, monospace, cursive, fantasy, system-ui, math, emoji, fangsong - at the end of the font or family string, to act as a fallback default as other fonts load. family: 'sans-serif',
};
P.defs = mergeOver(P.defs, defaultAttributes);let G = P.getters,
S = P.setters,
D = P.deltaSetters;G.size = function () {
return (this.sizeValue) ? `${this.sizeValue}${this.sizeMetric}` : this.sizeMetric;
};S.size = function (item) {
if (xt(item)) {
let res,
size = 0,
metric = 'medium';
if (item.indexOf('xx-small') >= 0) metric = 'xx-small';
else if (item.indexOf('x-small') >= 0) metric = 'x-small';
else if (item.indexOf('smaller') >= 0) metric = 'smaller';
else if (item.indexOf('small') >= 0) metric = 'small';
else if (item.indexOf('medium') >= 0) metric = 'medium';
else if (item.indexOf('xxx-large') >= 0) metric = 'xxx-large';
else if (item.indexOf('xx-large') >= 0) metric = 'xx-large';
else if (item.indexOf('x-large') >= 0) metric = 'x-large';
else if (item.indexOf('larger') >= 0) metric = 'larger';
else if (item.indexOf('large') >= 0) metric = 'large';
else {
size = 12;
metric = 'px'
}
let full, val, suffix;
let r = item.match(/(\d+\.\d+|\d+|\.\d+)(rem|em|rlh|lh|ex|cap|ch|ic|%|vw|vh|vmax|vmin|vi|vb|in|cm|mm|Q|pc|pt|px)?/i);
if (Array.isArray(r)) {
[full, val, suffix] = r;
if (val && suffix && val != '.') {
size = val;
metric = suffix;
}
}
else {
r = item.match(/\/(\d+\.\d+|\d+|\.\d+)(rem|em|rlh|lh|ex|cap|ch|ic|%|vw|vh|vmax|vmin|vi|vb|in|cm|mm|Q|pc|pt|px)?/i);
if (Array.isArray(r)) {
[full, val, suffix] = r;
if (val && suffix && val != '.') {
size = val;
metric = suffix;
}
}
}
if (size !== this.sizeValue) {
this.sizeValue = size;
this.dirtyFont = true;
}
if (metric !== this.sizeMetric) {
this.sizeMetric = metric;
this.dirtyFont = true;
}
}
};
S.sizeValue = function (item) {
if (xt(item) && item !== this.sizeValue) {
this.sizeValue = item;
this.dirtyFont = true;
}
}
S.sizeMetric = function (item) {
if (xt(item) && item !== this.sizeMetric) {
this.sizeMetric = item;
this.dirtyFont = true;
}
}font - pseudo-attribute which calls various functions to break down the font String into its constituent parts
S.font = function (item) {
if (xt(item)) {
S.style.call(this, item);
S.variant.call(this, item);
S.weight.call(this, item);
S.stretch.call(this, item);
S.size.call(this, item);
S.family.call(this, item);
}
};style
S.style = function (item) {
if (xt(item)) {
let v = 'normal';
v = (item.indexOf('italic') >= 0) ? 'italic' : v;
v = (item.indexOf('oblique') >= 0) ? 'oblique' : v;
if (v !== this.style) {
this.style = v;
this.dirtyFont = true;
}
}
};variant
S.variant = function (item) {
if (xt(item)) {
let v = 'normal';
v = (item.indexOf('small-caps') >= 0) ? 'small-caps' : v;
if (v !== this.variant) {
this.variant = v;
this.dirtyFont = true;
}
}
};weight
S.weight = function (item) {
if (xt(item)) {
let v = 'normal';Handling direct entry of numbers
if (item.toFixed) v = item;
else {
v = (item.indexOf('bold') >= 0) ? 'bold' : v;
v = (item.indexOf('lighter') >= 0) ? 'lighter' : v;
v = (item.indexOf('bolder') >= 0) ? 'bolder' : v;Putting spaces around the number should help identify it as a Weight value within the font string the string
v = (item.indexOf(' 100 ') >= 0) ? '100' : v;
v = (item.indexOf(' 200 ') >= 0) ? '200' : v;
v = (item.indexOf(' 300 ') >= 0) ? '300' : v;
v = (item.indexOf(' 400 ') >= 0) ? '400' : v;
v = (item.indexOf(' 500 ') >= 0) ? '500' : v;
v = (item.indexOf(' 600 ') >= 0) ? '600' : v;
v = (item.indexOf(' 700 ') >= 0) ? '700' : v;
v = (item.indexOf(' 800 ') >= 0) ? '800' : v;
v = (item.indexOf(' 900 ') >= 0) ? '900' : v;Also need to capture instances where a number value has been directly set with no other font attributes around it
v = (/^\d00$/.test(item)) ? item : v;
}
if (v !== this.weight) {
this.weight = v;
this.dirtyFont = true;
}
}
};stretch
S.stretch = function (item) {
if (xt(item)) {
let v = 'normal';
v = (item.indexOf('semi-condensed') >= 0) ? 'semi-condensed' : v;
v = (item.indexOf('condensed') >= 0) ? 'condensed' : v;
v = (item.indexOf('extra-condensed') >= 0) ? 'extra-condensed' : v;
v = (item.indexOf('ultra-condensed') >= 0) ? 'ultra-condensed' : v;
v = (item.indexOf('semi-expanded') >= 0) ? 'semi-expanded' : v;
v = (item.indexOf('expanded') >= 0) ? 'expanded' : v;
v = (item.indexOf('extra-expanded') >= 0) ? 'extra-expanded' : v;
v = (item.indexOf('ultra-expanded') >= 0) ? 'ultra-expanded' : v;
if (v !== this.stretch) {
this.stretch = v;
this.dirtyFont = true;
}
}
};family
P.rfsTestArray1 = ['italic','oblique','small-caps','normal','bold','lighter','bolder','ultra-condensed','extra-condensed','semi-condensed','condensed','ultra-expanded','extra-expanded','semi-expanded','expanded','xx-small','x-small','small','medium','xxx-large','xx-large','x-large','large'];
P.rfsTestArray2 = ['0','1','2','3','4','5','6','7','8','9'];
S.family = function (item) {
if (xt(item)) {
let v = 'sans-serif';
let itemArray = item.split(' '),
len = itemArray.length;
if (len === 1) v = item;
let counter = 0,
flag = true;
while (flag) {
if (counter === len) flag = false;
else {
let el = itemArray[counter];
if (!el.length) counter++;
else if (this.rfsTestArray1.indexOf(el) >= 0) counter++;
else if (this.rfsTestArray2.indexOf(el[0]) >= 0) counter++;
else flag = false;
}
}
if (counter < len) v = itemArray.slice(counter).join(' ');
if (v !== this.family) {
this.family = v;
this.dirtyFont = true;
}
}
};P.getFontString = function() {
if (!this.dirtyFont && this.temperedFontString) return this.temperedFontString;
else return this.buildFont();
}
P.updateMetadata = function (scale, lineHeight, host) {
if (scale && scale.toFixed && scale > 0 && scale !== this.scale) {
this.scale = scale;
this.dirtyFont = true;
}
if (lineHeight && lineHeight.toFixed && lineHeight > 0 && lineHeight !== this.lineHeight) {
this.lineHeight = lineHeight;
this.dirtyFont = true;
}
let currentHost = (this.host && this.host.type && this.host.type === 'Cell') ? this.host.name : '';
if (host && host.type && host.type === 'Cell' && host.name !== currentHost) {
this.host = host;
this.dirtyFont = true;
}
};
P.calculateSize = function () {
if (this.host) {
let {scale, lineHeight, host, sizeValue, sizeMetric} = this;
let gcfs = host.getComputedFontSizes(),
parentSize, rootSize, viewportWidth, viewportHeight;
if (!gcfs) {
if (['in', 'cm', 'mm', 'Q', 'pc', 'pt', 'px'].indexOf(sizeMetric) < 0) {
this.dirtyFont = true;
return '12px';
}
}
else {
[parentSize, rootSize, viewportWidth, viewportHeight] = host.getComputedFontSizes();
}
if (isNaN(sizeValue)) sizeValue = 12;
let res = parentSize;
switch (sizeMetric) {
case 'rem' :
res = rootSize * sizeValue;
break;
case 'em' :
res = parentSize * sizeValue;
break;
case 'rlh' :
res = (rootSize * lineHeight) * sizeValue;
break;
case 'lh' :
res = (parentSize * lineHeight) * sizeValue;
break;
case 'ex' :
res = (parentSize / 2) * sizeValue;
break;
case 'cap' :
res = parentSize * sizeValue;
break;
case 'ch' :
res = parentSize * sizeValue;
break;
case 'ic' :
res = parentSize * sizeValue;
break;
case '%' :
res = (parentSize / 100) * sizeValue;
break;
case 'vw' :
res = (viewportWidth / 100) * sizeValue;
break;
case 'vh' :
res = (viewportHeight / 100) * sizeValue;
break;
case 'vmax' :
res = (Math.max(viewportWidth, viewportHeight) / 100) * sizeValue;
break;
case 'vmin' :
res = (Math.min(viewportWidth, viewportHeight) / 100) * sizeValue;
break;
case 'vi' :
res = (viewportWidth / 100) * sizeValue;
break;
case 'vb' :
res = (viewportHeight / 100) * sizeValue;
break;
case 'in' :
res = 96 * sizeValue;
break;
case 'cm' :
res = 37.8 * sizeValue;
break;
case 'mm' :
res = 3.78 * sizeValue;
break;
case 'Q' :
res = 0.95 * sizeValue;
break;
case 'pc' :
res = 16 * sizeValue;
break;
case 'pt' :
res = 1.33 * sizeValue;
break;
case 'px' :
res = sizeValue;
break;
case 'xx-small' :
res = 0.6 * parentSize;
break;
case 'x-small' :
res = 0.75 * parentSize;
break;
case 'smaller' :
res = 0.8 * parentSize;
break;
case 'small' :
res = 0.89 * parentSize;
break;
case 'xxx-large' :
res = 3 * parentSize;
break;
case 'xx-large' :
res = 2 * parentSize;
break;
case 'x-large' :
res = 1.5 * parentSize;
break;
case 'larger' :
res = 1.3 * parentSize;
break;
case 'large' :
res = 1.2 * parentSize;
break;
}
return `${res * scale}px`;
}
return '12px';
}P.buildFont = function () {
this.dirtyFont = false;
let font = ''
if (this.style !== 'normal') font += `${this.style} `;
if (this.variant !== 'normal') font += `${this.variant} `;
if (this.weight !== 'normal') font += `${this.weight} `;
if (this.stretch !== 'normal') font += `${this.stretch} `;
font += `${this.calculateSize()} `;
font += `${this.family}`;Temper the font string. Submit it to a canvas context engine to see what it makes of it
let myCell = requestCell();
myCell.engine.font = font;
font = myCell.engine.font;
releaseCell(myCell);
this.temperedFontString = font;
return font;
};update - sets items, then calls buildFont
P.update = function (items) {
if (items) this.set(items);
return this.getFontString();
};const makeFontAttributes = function (items) {
return new FontAttributes(items);
};
constructors.FontAttributes = FontAttributes;export {
makeFontAttributes,
};