@mapka/font-sdf 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/curve.js ADDED
@@ -0,0 +1,307 @@
1
+ function point_d(x, y) {
2
+ return [x, y];
3
+ }
4
+
5
+ function calc_sq_distance(x1, y1, x2, y2) {
6
+ const dx = x2 - x1;
7
+ const dy = y2 - y1;
8
+ return dx * dx + dy * dy;
9
+ }
10
+
11
+ export function Curve4Div() {}
12
+
13
+ Curve4Div.curve_collinearity_epsilon = 1e-30;
14
+ Curve4Div.curve_angle_tolerance_epsilon = 0.01;
15
+ Curve4Div.curve_recursion_limit = 32;
16
+
17
+ Curve4Div.prototype.approximation_scale = 1.0;
18
+ Curve4Div.prototype.angle_tolerance = 0.0;
19
+ Curve4Div.prototype.cusp_limit = 0.0;
20
+
21
+ Curve4Div.prototype.init = function (x1, y1, x2, y2, x3, y3, x4, y4) {
22
+ this.points = [];
23
+ this.distance_tolerance_square = 0.5 / this.approximation_scale;
24
+ this.distance_tolerance_square *= this.distance_tolerance_square;
25
+ this.bezier(x1, y1, x2, y2, x3, y3, x4, y4);
26
+ };
27
+
28
+ Curve4Div.prototype.bezier = function (x1, y1, x2, y2, x3, y3, x4, y4) {
29
+ this.points.push(point_d(x1, y1));
30
+ this.recursive_bezier(x1, y1, x2, y2, x3, y3, x4, y4, 0);
31
+ this.points.push(point_d(x4, y4));
32
+ };
33
+
34
+ Curve4Div.prototype.recursive_bezier = function (x1, y1, x2, y2, x3, y3, x4, y4, level) {
35
+ if (level > Curve4Div.curve_recursion_limit) {
36
+ return;
37
+ }
38
+
39
+ // Calculate all the mid-points of the line segments
40
+ const x12 = (x1 + x2) / 2;
41
+ const y12 = (y1 + y2) / 2;
42
+ const x23 = (x2 + x3) / 2;
43
+ const y23 = (y2 + y3) / 2;
44
+ const x34 = (x3 + x4) / 2;
45
+ const y34 = (y3 + y4) / 2;
46
+ const x123 = (x12 + x23) / 2;
47
+ const y123 = (y12 + y23) / 2;
48
+ const x234 = (x23 + x34) / 2;
49
+ const y234 = (y23 + y34) / 2;
50
+ const x1234 = (x123 + x234) / 2;
51
+ const y1234 = (y123 + y234) / 2;
52
+
53
+ // Try to approximate the full cubic curve by a single straight line
54
+ const dx = x4 - x1;
55
+ const dy = y4 - y1;
56
+
57
+ let d2 = Math.abs((x2 - x4) * dy - (y2 - y4) * dx);
58
+ let d3 = Math.abs((x3 - x4) * dy - (y3 - y4) * dx);
59
+
60
+ let da1;
61
+ let da2;
62
+ let k;
63
+
64
+ switch (
65
+ (Math.floor(d2 > Curve4Div.curve_collinearity_epsilon) << 1) +
66
+ Math.floor(d3 > Curve4Div.curve_collinearity_epsilon)
67
+ ) {
68
+ case 0:
69
+ // All collinear OR p1==p4
70
+ k = dx * dx + dy * dy;
71
+ if (k === 0) {
72
+ d2 = calc_sq_distance(x1, y1, x2, y2);
73
+ d3 = calc_sq_distance(x4, y4, x3, y3);
74
+ } else {
75
+ k = 1 / k;
76
+ da1 = x2 - x1;
77
+ da2 = y2 - y1;
78
+ d2 = k * (da1 * dx + da2 * dy);
79
+ da1 = x3 - x1;
80
+ da2 = y3 - y1;
81
+ d3 = k * (da1 * dx + da2 * dy);
82
+ if (d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1) {
83
+ // Simple collinear case, 1---2---3---4
84
+ // We can leave just two endpoints
85
+ return;
86
+ }
87
+ if (d2 <= 0) {
88
+ d2 = calc_sq_distance(x2, y2, x1, y1);
89
+ } else if (d2 >= 1) {
90
+ d2 = calc_sq_distance(x2, y2, x4, y4);
91
+ } else {
92
+ d2 = calc_sq_distance(x2, y2, x1 + d2 * dx, y1 + d2 * dy);
93
+ }
94
+
95
+ if (d3 <= 0) {
96
+ d3 = calc_sq_distance(x3, y3, x1, y1);
97
+ } else if (d3 >= 1) {
98
+ d3 = calc_sq_distance(x3, y3, x4, y4);
99
+ } else {
100
+ d3 = calc_sq_distance(x3, y3, x1 + d3 * dx, y1 + d3 * dy);
101
+ }
102
+ }
103
+
104
+ if (d2 > d3) {
105
+ if (d2 < this.distance_tolerance_square) {
106
+ this.points.push(point_d(x2, y2));
107
+ return;
108
+ }
109
+ } else {
110
+ if (d3 < this.distance_tolerance_square) {
111
+ this.points.push(point_d(x3, y3));
112
+ return;
113
+ }
114
+ }
115
+ break;
116
+
117
+ case 1:
118
+ // p1,p2,p4 are collinear, p3 is significant
119
+ if (d3 * d3 <= this.distance_tolerance_square * (dx * dx + dy * dy)) {
120
+ if (this.angle_tolerance < Curve4Div.curve_angle_tolerance_epsilon) {
121
+ this.points.push(point_d(x23, y23));
122
+ return;
123
+ }
124
+
125
+ // Angle Condition
126
+ da1 = Math.abs(Math.atan2(y4 - y3, x4 - x3) - Math.atan2(y3 - y2, x3 - x2));
127
+ if (da1 >= Math.PI) da1 = 2 * Math.PI - da1;
128
+
129
+ if (da1 < this.angle_tolerance) {
130
+ this.points.push(point_d(x2, y2));
131
+ this.points.push(point_d(x3, y3));
132
+ return;
133
+ }
134
+
135
+ if (this.cusp_limit !== 0.0) {
136
+ if (da1 > this.cusp_limit) {
137
+ this.points.push(point_d(x3, y3));
138
+ return;
139
+ }
140
+ }
141
+ }
142
+ break;
143
+
144
+ case 2:
145
+ // p1,p3,p4 are collinear, p2 is significant
146
+ if (d2 * d2 <= this.distance_tolerance_square * (dx * dx + dy * dy)) {
147
+ if (this.angle_tolerance < Curve4Div.curve_angle_tolerance_epsilon) {
148
+ this.points.push(point_d(x23, y23));
149
+ return;
150
+ }
151
+
152
+ // Angle Condition
153
+ da1 = Math.abs(Math.atan2(y3 - y2, x3 - x2) - Math.atan2(y2 - y1, x2 - x1));
154
+ if (da1 >= Math.PI) da1 = 2 * Math.PI - da1;
155
+
156
+ if (da1 < this.angle_tolerance) {
157
+ this.points.push(point_d(x2, y2));
158
+ this.points.push(point_d(x3, y3));
159
+ return;
160
+ }
161
+
162
+ if (this.cusp_limit !== 0.0) {
163
+ if (da1 > this.cusp_limit) {
164
+ this.points.push(point_d(x2, y2));
165
+ return;
166
+ }
167
+ }
168
+ }
169
+ break;
170
+
171
+ case 3:
172
+ // Regular case
173
+ if ((d2 + d3) * (d2 + d3) <= this.distance_tolerance_square * (dx * dx + dy * dy)) {
174
+ // If the curvature doesn't exceed the distance_tolerance value
175
+ // we tend to finish subdivisions.
176
+ if (this.angle_tolerance < Curve4Div.curve_angle_tolerance_epsilon) {
177
+ this.points.push(point_d(x23, y23));
178
+ return;
179
+ }
180
+
181
+ // Angle & Cusp Condition
182
+ k = Math.atan2(y3 - y2, x3 - x2);
183
+ da1 = Math.abs(k - Math.atan2(y2 - y1, x2 - x1));
184
+ da2 = Math.abs(Math.atan2(y4 - y3, x4 - x3) - k);
185
+ if (da1 >= Math.PI) da1 = 2 * Math.PI - da1;
186
+ if (da2 >= Math.PI) da2 = 2 * Math.PI - da2;
187
+
188
+ if (da1 + da2 < this.angle_tolerance) {
189
+ // Finally we can stop the recursion
190
+ this.points.push(point_d(x23, y23));
191
+ return;
192
+ }
193
+
194
+ if (this.cusp_limit !== 0.0) {
195
+ if (da1 > this.cusp_limit) {
196
+ this.points.push(point_d(x2, y2));
197
+ return;
198
+ }
199
+
200
+ if (da2 > this.cusp_limit) {
201
+ this.points.push(point_d(x3, y3));
202
+ return;
203
+ }
204
+ }
205
+ }
206
+ break;
207
+ }
208
+
209
+ // Continue subdivision
210
+ this.recursive_bezier(x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1);
211
+ this.recursive_bezier(x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1);
212
+ };
213
+
214
+
215
+ export function Curve3Div() {}
216
+
217
+ Curve3Div.curve_collinearity_epsilon = 1e-30;
218
+ Curve3Div.curve_angle_tolerance_epsilon = 0.01;
219
+ Curve3Div.curve_recursion_limit = 32;
220
+
221
+ Curve3Div.prototype.approximation_scale = 1.0;
222
+ Curve3Div.prototype.angle_tolerance = 0.0;
223
+ Curve3Div.prototype.cusp_limit = 0.0;
224
+
225
+ Curve3Div.prototype.init = function (x1, y1, x2, y2, x3, y3) {
226
+ this.points = [];
227
+ this.distance_tolerance_square = 0.5 / this.approximation_scale;
228
+ this.distance_tolerance_square *= this.distance_tolerance_square;
229
+ this.bezier(x1, y1, x2, y2, x3, y3);
230
+ };
231
+
232
+ Curve3Div.prototype.bezier = function (x1, y1, x2, y2, x3, y3) {
233
+ this.points.push(point_d(x1, y1));
234
+ this.recursive_bezier(x1, y1, x2, y2, x3, y3, 0);
235
+ this.points.push(point_d(x3, y3));
236
+ };
237
+
238
+ Curve3Div.prototype.recursive_bezier = function (x1, y1, x2, y2, x3, y3, level) {
239
+ if (level > Curve3Div.curve_recursion_limit) {
240
+ return;
241
+ }
242
+
243
+ // Calculate all the mid-points of the line segments
244
+ const x12 = (x1 + x2) / 2;
245
+ const y12 = (y1 + y2) / 2;
246
+ const x23 = (x2 + x3) / 2;
247
+ const y23 = (y2 + y3) / 2;
248
+ const x123 = (x12 + x23) / 2;
249
+ const y123 = (y12 + y23) / 2;
250
+
251
+ const dx = x3 - x1;
252
+ const dy = y3 - y1;
253
+ let d = Math.abs((x2 - x3) * dy - (y2 - y3) * dx);
254
+ let da;
255
+
256
+ if (d > Curve3Div.curve_collinearity_epsilon) {
257
+ // Regular case
258
+ if (d * d <= this.distance_tolerance_square * (dx * dx + dy * dy)) {
259
+ // If the curvature doesn't exceed the distance_tolerance value
260
+ // we tend to finish subdivisions.
261
+ if (this.angle_tolerance < Curve3Div.curve_angle_tolerance_epsilon) {
262
+ this.points.push(point_d(x123, y123));
263
+ return;
264
+ }
265
+
266
+ // Angle & Cusp Condition
267
+ da = Math.abs(Math.atan2(y3 - y2, x3 - x2) - Math.atan2(y2 - y1, x2 - x1));
268
+ if (da >= Math.PI) {
269
+ da = 2 * Math.PI - da;
270
+ }
271
+
272
+ if (da < this.angle_tolerance) {
273
+ // Finally we can stop the recursion
274
+ this.points.push(point_d(x123, y123));
275
+ return;
276
+ }
277
+ }
278
+ } else {
279
+ // Collinear case
280
+ da = dx * dx + dy * dy;
281
+ if (da === 0) {
282
+ d = calc_sq_distance(x1, y1, x2, y2);
283
+ } else {
284
+ d = ((x2 - x1) * dx + (y2 - y1) * dy) / da;
285
+ if (d > 0 && d < 1) {
286
+ // Simple collinear case, 1---2---3
287
+ // We can leave just two endpoints
288
+ return;
289
+ }
290
+ if (d <= 0) {
291
+ d = calc_sq_distance(x2, y2, x1, y1);
292
+ } else if (d >= 1) {
293
+ d = calc_sq_distance(x2, y2, x3, y3);
294
+ } else {
295
+ d = calc_sq_distance(x2, y2, x1 + d * dx, y1 + d * dy);
296
+ }
297
+ }
298
+ if (d < this.distance_tolerance_square) {
299
+ this.points.push(point_d(x2, y2));
300
+ return;
301
+ }
302
+ }
303
+
304
+ // Continue subdivision
305
+ this.recursive_bezier(x1, y1, x12, y12, x123, y123, level + 1);
306
+ this.recursive_bezier(x123, y123, x23, y23, x3, y3, level + 1);
307
+ };
package/src/font.ts ADDED
@@ -0,0 +1,99 @@
1
+ import Pbf from "pbf";
2
+ import { load } from "opentype.js";
3
+ import type { Font } from "opentype.js";
4
+ import { decode, encode, type Glyph, type FontStack } from "./pbf.js";
5
+ import { writeglyphs } from "./glyphs.js";
6
+ import { glyphToSDF } from "./sdf.js";
7
+
8
+ export async function readFont(filename: string): Promise<Font> {
9
+ return new Promise<Font>((resolve, reject) => {
10
+ load(filename, (err, font) => {
11
+ if (err || !font) {
12
+ reject(err);
13
+ } else {
14
+ resolve(font);
15
+ }
16
+ });
17
+ });
18
+ }
19
+
20
+ const fontSize = 24;
21
+ const buffer = 3;
22
+ const cutoff = 2 / 8;
23
+
24
+ /**
25
+ * Convert opentype font to glyphs sdf.
26
+ */
27
+ export function fontToGlyphs(font: Font, from = 0, to = 65535) {
28
+ const pbf = new Pbf();
29
+
30
+
31
+ const family = font.tables.name.preferredFamily?.en || font.tables.name.fontFamily?.en;
32
+ const style = font.tables.name.preferredSubfamily?.en || font.tables.name.fontSubfamily?.en;
33
+
34
+ const basename = `${family} ${style}`;
35
+
36
+ const fontStack: FontStack = {
37
+ name: basename,
38
+ range: `${from}-${to}`,
39
+ glyphs: [],
40
+ };
41
+
42
+ for (let i = from; i <= to; i++) {
43
+ const char = String.fromCharCode(i);
44
+ const glyph = font.charToGlyph(char);
45
+
46
+ if (glyph.index > 0) {
47
+ const sdf = glyphToSDF(
48
+ font,
49
+ glyph,
50
+ fontSize,
51
+ buffer,
52
+ cutoff
53
+ );
54
+
55
+ /**
56
+ * Base sdf glyph
57
+ */
58
+ const sdfGlyph: Glyph = {
59
+ id: i,
60
+ bitmap: null,
61
+ width: sdf.glyphWidth,
62
+ height: sdf.glyphHeight,
63
+ left: sdf.glyphBearingX,
64
+ top: sdf.glyphTop,
65
+ advance: sdf.glyphAdvance,
66
+ };
67
+
68
+ /**
69
+ * If the glyph has no bitmap data, don't include it.
70
+ * For example, the space character has no bitmap data.
71
+ */
72
+ if (sdf.data) {
73
+ sdfGlyph.bitmap = Buffer.from(sdf.data);
74
+ }
75
+
76
+ fontStack.glyphs.push(sdfGlyph);
77
+ }
78
+ }
79
+ writeglyphs({ stacks: [fontStack] }, pbf);
80
+ const view = pbf.finish();
81
+
82
+ return Buffer.from(view);
83
+ }
84
+
85
+ /**
86
+ * Filter glyphs by range.
87
+ */
88
+ export function glyphsRange(fontStack: Buffer, from = 0, to = 65535): Buffer {
89
+ const result = decode(fontStack);
90
+
91
+ const {
92
+ stacks: [first],
93
+ } = result;
94
+
95
+ first.range = `${from}-${to}`;
96
+ first.glyphs = first.glyphs.filter((glyph) => glyph.id >= from && glyph.id <= to);
97
+
98
+ return encode(result);
99
+ }
package/src/glyphs.js ADDED
@@ -0,0 +1,47 @@
1
+ // code generated by pbf v4.0.1
2
+
3
+ export function readglyph(pbf, end) {
4
+ return pbf.readFields(readglyphField, {id: 0, bitmap: undefined, width: 0, height: 0, left: 0, top: 0, advance: 0}, end);
5
+ }
6
+ function readglyphField(tag, obj, pbf) {
7
+ if (tag === 1) obj.id = pbf.readVarint();
8
+ else if (tag === 2) obj.bitmap = pbf.readBytes();
9
+ else if (tag === 3) obj.width = pbf.readVarint();
10
+ else if (tag === 4) obj.height = pbf.readVarint();
11
+ else if (tag === 5) obj.left = pbf.readSVarint();
12
+ else if (tag === 6) obj.top = pbf.readSVarint();
13
+ else if (tag === 7) obj.advance = pbf.readVarint();
14
+ }
15
+ export function writeglyph(obj, pbf) {
16
+ if (obj.id) pbf.writeVarintField(1, obj.id);
17
+ if (obj.bitmap != null) pbf.writeBytesField(2, obj.bitmap);
18
+ if (obj.width) pbf.writeVarintField(3, obj.width);
19
+ if (obj.height) pbf.writeVarintField(4, obj.height);
20
+ if (obj.left) pbf.writeSVarintField(5, obj.left);
21
+ if (obj.top) pbf.writeSVarintField(6, obj.top);
22
+ if (obj.advance) pbf.writeVarintField(7, obj.advance);
23
+ }
24
+
25
+ export function readfontstack(pbf, end) {
26
+ return pbf.readFields(readfontstackField, {name: "", range: "", glyphs: []}, end);
27
+ }
28
+ function readfontstackField(tag, obj, pbf) {
29
+ if (tag === 1) obj.name = pbf.readString();
30
+ else if (tag === 2) obj.range = pbf.readString();
31
+ else if (tag === 3) obj.glyphs.push(readglyph(pbf, pbf.readVarint() + pbf.pos));
32
+ }
33
+ export function writefontstack(obj, pbf) {
34
+ if (obj.name) pbf.writeStringField(1, obj.name);
35
+ if (obj.range) pbf.writeStringField(2, obj.range);
36
+ if (obj.glyphs) for (const item of obj.glyphs) pbf.writeMessage(3, writeglyph, item);
37
+ }
38
+
39
+ export function readglyphs(pbf, end) {
40
+ return pbf.readFields(readglyphsField, {stacks: []}, end);
41
+ }
42
+ function readglyphsField(tag, obj, pbf) {
43
+ if (tag === 1) obj.stacks.push(readfontstack(pbf, pbf.readVarint() + pbf.pos));
44
+ }
45
+ export function writeglyphs(obj, pbf) {
46
+ if (obj.stacks) for (const item of obj.stacks) pbf.writeMessage(1, writefontstack, item);
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export {decode, encode, debug} from './pbf.js';
2
+ export {pathToSDF} from './sdf.js';
3
+ export {fontToGlyphs, glyphsRange, readFont} from './font.js';
4
+
5
+ export type {
6
+ Glyph,
7
+ FontStack,
8
+ Glyphs
9
+ } from './pbf.js';
package/src/pbf.ts ADDED
@@ -0,0 +1,69 @@
1
+ import Pbf from "pbf";
2
+ import {
3
+ readglyphs,
4
+ writeglyphs
5
+ } from "./glyphs.js";
6
+
7
+ export interface Glyph {
8
+ id: number;
9
+ bitmap: Buffer | null;
10
+ width: number;
11
+ height: number;
12
+ left: number;
13
+ top: number;
14
+ advance: number;
15
+ }
16
+
17
+ export interface FontStack {
18
+ name?: string;
19
+ range?: string;
20
+ glyphs: Glyph[];
21
+ }
22
+
23
+ export interface Glyphs {
24
+ stacks: FontStack[];
25
+ }
26
+
27
+ /**
28
+ *
29
+ * @param buffer A PBF buffer.
30
+ * @returns {Glyphs} A Glyphs message.
31
+ */
32
+ export function decode(buffer: Buffer): Glyphs {
33
+ const pbf = new Pbf(buffer);
34
+ return readglyphs(pbf);
35
+ }
36
+
37
+ /**
38
+ * Encode a Glyphs message.
39
+ * @param {Glyphs} message A Glyphs message.
40
+ * @returns {Buffer} A PBF buffer.
41
+ */
42
+ export function encode(message: Glyphs): Buffer {
43
+ const pbf = new Pbf();
44
+ writeglyphs(message, pbf);
45
+ const view = pbf.finish();
46
+
47
+ return Buffer.from(view);
48
+ }
49
+
50
+ /**
51
+ * Debug a PBF.
52
+ * @param {Buffer} buffer A PBF buffer.
53
+ * @param {boolean} decoded Decode the buffer before stringify.
54
+ */
55
+ export function debug(buffer: Buffer, decoded = true) {
56
+ let result: Buffer | Glyphs = buffer
57
+ if (decoded) {
58
+ result = decode(buffer);
59
+ }
60
+
61
+ return JSON.stringify(
62
+ result,
63
+ (k, v) => {
64
+ if (k !== "bitmap") return v;
65
+ return v ? v.data.length : v;
66
+ },
67
+ 2,
68
+ );
69
+ }