Skip to content

Commit 609e5d5

Browse files
committed
Accept character set parameter, try to find closest match
1 parent fb1e74d commit 609e5d5

File tree

4 files changed

+1405
-24
lines changed

4 files changed

+1405
-24
lines changed

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"version": "2.0.0-beta.5",
2727
"dependencies": {
2828
"@davepagurek/bezier-path": "^0.0.2",
29+
"@japont/unicode-range": "^1.0.0",
2930
"acorn": "^8.12.1",
3031
"acorn-walk": "^8.3.4",
3132
"colorjs.io": "^0.5.2",

src/type/p5.Font.js

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import { textCoreConstants } from './textCore';
66
import * as constants from '../core/constants';
7+
import { UnicodeRange } from '@japont/unicode-range';
8+
import { unicodeRanges } from './unicodeRanges';
79

810
/*
911
API:
@@ -789,7 +791,7 @@ function parseCreateArgs(...args/*path, name, onSuccess, onError*/) {
789791
}
790792

791793
// get the callbacks/descriptors if any
792-
let success, error, descriptors;
794+
let success, error, options;
793795
for (let i = 0; i < args.length; i++) {
794796
const arg = args[i];
795797
if (typeof arg === 'function') {
@@ -800,11 +802,11 @@ function parseCreateArgs(...args/*path, name, onSuccess, onError*/) {
800802
}
801803
}
802804
else if (typeof arg === 'object') {
803-
descriptors = arg;
805+
options = arg;
804806
}
805807
}
806808

807-
return { path, name, success, error, descriptors };
809+
return { path, name, success, error, options };
808810
}
809811

810812
function font(p5, fn) {
@@ -878,7 +880,7 @@ function font(p5, fn) {
878880
* @param {String} path path of the font or CSS file to be loaded, or a CSS `@font-face` string.
879881
* @param {String} [name] An alias that can be used for this font in `textFont()`. Defaults to the name in the font's metadata.
880882
* @param {Object} [options] An optional object with extra CSS font face descriptors, or p5.js font settings.
881-
* @param {Number} [options.index] An optional index specifying which font from a CSS file to use. Defaults to the last one in the file.
883+
* @param {String|Array<String>} [options.sets] (Experimental) An optional string of list of strings with Unicode character set names that should be included. When a CSS file is used as the font, it may contain multiple font files. The font best matching the requested character sets will be picked.
882884
* @param {Function} [successCallback] function called with the
883885
* <a href="#/p5.Font">p5.Font</a> object after it
884886
* loads.
@@ -984,7 +986,7 @@ function font(p5, fn) {
984986
*/
985987
fn.loadFont = async function (...args/*path, name, onSuccess, onError, descriptors*/) {
986988

987-
let { path, name, success, error, options: { index, ...descriptors } = {} } = parseCreateArgs(...args);
989+
let { path, name, success, error, options: { sets, ...descriptors } = {} } = parseCreateArgs(...args);
988990

989991
let isCSS = path.includes('@font-face');
990992

@@ -1000,7 +1002,7 @@ function font(p5, fn) {
10001002
if (isCSS) {
10011003
const stylesheet = new CSSStyleSheet();
10021004
await stylesheet.replace(path);
1003-
const fontPromises = [];
1005+
const possibleFonts = [];
10041006
for (const rule of stylesheet.cssRules) {
10051007
if (rule instanceof CSSFontFaceRule) {
10061008
const style = rule.style;
@@ -1016,28 +1018,79 @@ function font(p5, fn) {
10161018
.join('');
10171019
fontDescriptors[camelCaseKey] = style.getPropertyValue(key);
10181020
}
1019-
fontPromises.push((async () => {
1020-
let fontData;
1021-
try {
1022-
const urlMatch = /url\(([^\)]+)\)/.exec(src);
1023-
if (urlMatch) {
1024-
let url = urlMatch[1];
1025-
if (/^['"]/.exec(url) && url.at(0) === url.at(-1)) {
1026-
url = url.slice(1, -1)
1021+
possibleFonts.push({
1022+
name,
1023+
src,
1024+
fontDescriptors,
1025+
load: async () => {
1026+
let fontData;
1027+
try {
1028+
const urlMatch = /url\(([^\)]+)\)/.exec(src);
1029+
if (urlMatch) {
1030+
let url = urlMatch[1];
1031+
if (/^['"]/.exec(url) && url.at(0) === url.at(-1)) {
1032+
url = url.slice(1, -1)
1033+
}
1034+
fontData = await fn.parseFontData(url);
10271035
}
1028-
fontData = await fn.parseFontData(url);
1029-
}
1030-
} catch (_e) {}
1031-
return create(this, name, src, fontDescriptors, fontData)
1032-
})());
1036+
} catch (_e) {}
1037+
return create(this, name, src, fontDescriptors, fontData)
1038+
}
1039+
});
10331040
}
10341041
}
1035-
if (index !== undefined) {
1036-
return await fontPromises[index]
1037-
} else {
1038-
const fonts = await Promise.all(fontPromises);
1039-
return fonts.findLast(f => f.data) || fonts[0]; // TODO: handle multiple faces?
1042+
1043+
// TODO: handle multiple font faces?
1044+
sets = sets || ['latin']; // Default to latin for now if omitted
1045+
const requestedGroups = (sets instanceof Array ? sets : [sets])
1046+
.map(s => s.toLowerCase());
1047+
// Grab thr named groups with names that include the requested keywords
1048+
const requestedCategories = unicodeRanges
1049+
.filter((r) => requestedGroups.some(
1050+
g => r.category.includes(g) &&
1051+
// Only include extended character sets if specifically requested
1052+
r.category.includes('ext') === g.includes('ext')
1053+
));
1054+
const requestedRanges = new Set(
1055+
UnicodeRange.parse(
1056+
requestedCategories.map((c) => `U+${c.hexrange[0]}-${c.hexrange[1]}`)
1057+
)
1058+
);
1059+
let closestRangeOverlap = 0;
1060+
let closestDescriptorOverlap = 0;
1061+
let closestMatch = undefined;
1062+
for (const font of possibleFonts) {
1063+
if (!font.fontDescriptors.unicodeRange) continue;
1064+
const fontRange = new Set(
1065+
UnicodeRange.parse(
1066+
font.fontDescriptors.unicodeRange.split(/,\s*/g)
1067+
)
1068+
);
1069+
const rangeOverlap = [...fontRange.values()]
1070+
.filter(v => requestedRanges.has(v))
1071+
.length;
1072+
1073+
const targetDescriptors = {
1074+
// Default to normal style at regular weight
1075+
style: 'normal',
1076+
weight: 400,
1077+
// Override from anything else passed in
1078+
...descriptors
1079+
};
1080+
const descriptorOverlap = Object.keys(font.fontDescriptors)
1081+
.filter(k => font.fontDescriptors[k] === targetDescriptors[k])
1082+
.length;
1083+
1084+
if (
1085+
descriptorOverlap > closestDescriptorOverlap ||
1086+
(descriptorOverlap === closestDescriptorOverlap && rangeOverlap >= closestRangeOverlap)
1087+
) {
1088+
closestDescriptorOverlap = descriptorOverlap
1089+
closestRangeOverlap = rangeOverlap;
1090+
closestMatch = font;
1091+
}
10401092
}
1093+
return (closestMatch || possibleFonts.at(-1))?.load();
10411094
}
10421095

10431096
let pfont;

0 commit comments

Comments
 (0)