47 |
48 | ---
49 | ### Installation
50 |
51 | ```
52 | npm i uwrap
53 | ```
54 |
55 | or
56 |
57 | ```html
58 |
59 | ```
60 |
61 | ---
62 | ### API
63 |
64 | See [uWrap.d.ts](https://github.com/leeoniya/uWrap/blob/main/dist/uWrap.d.ts) TypeScript def.
65 |
66 | ---
67 | ### Usage
68 |
69 | ```js
70 | // import fn for wrapping variable-width fonts using pre-line strategy
71 | import { varPreLine } from 'uwrap';
72 |
73 | // create a Canvas2D context with desired font settings
74 | let ctx = document.createElement('canvas').getContext('2d');
75 | ctx.font = "14px Inter, sans-serif";
76 | ctx.letterSpacing = '0.15px';
77 |
78 | // init util fns
79 | const { count, test, split } = varPreLine(ctx);
80 |
81 | // example text
82 | let text = 'The quick brown fox jumps over the lazy dog.';
83 |
84 | // count lines
85 | let numLines = count(text, width);
86 |
87 | // test if text will wrap
88 | let willWrap = test(text, width);
89 |
90 | // split lines (with optional limit)
91 | let lines = split(text, width, 3);
92 | ```
--------------------------------------------------------------------------------
/demo/canvas-txt.mjs:
--------------------------------------------------------------------------------
1 | function B({
2 | ctx: e,
3 | line: c,
4 | spaceWidth: p,
5 | spaceChar: n,
6 | width: a
7 | }) {
8 | const i = c.trim(), o = i.split(/\s+/), s = o.length - 1;
9 | if (s === 0)
10 | return i;
11 | const m = e.measureText(o.join("")).width, d = (a - m) / p, b = Math.floor(d / s);
12 | if (d < 1)
13 | return i;
14 | const r = n.repeat(b);
15 | return o.join(r);
16 | }
17 | const W = " ";
18 | function k({
19 | ctx: e,
20 | text: c,
21 | justify: p,
22 | width: n
23 | }) {
24 | const a = /* @__PURE__ */ new Map(), i = (r) => {
25 | let g = a.get(r);
26 | return g !== void 0 || (g = e.measureText(r).width, a.set(r, g)), g;
27 | };
28 | let o = [], s = c.split(`
29 | `);
30 | const m = p ? i(W) : 0;
31 | let d = 0, b = 0;
32 | for (const r of s) {
33 | let g = i(r);
34 | const y = r.length;
35 | if (g <= n) {
36 | o.push(r);
37 | continue;
38 | }
39 | let h = r, t, f, l = "";
40 | for (; g > n; ) {
41 | if (d++, t = b, f = t === 0 ? 0 : i(r.substring(0, t)), f < n)
42 | for (; f < n && t < y && (t++, f = i(h.substring(0, t)), t !== y); )
43 | ;
44 | else if (f > n)
45 | for (; f > n && (t = Math.max(1, t - 1), f = i(h.substring(0, t)), !(t === 0 || t === 1)); )
46 | ;
47 | if (b = Math.round(
48 | b + (t - b) / d
49 | ), t--, t > 0) {
50 | let u = t;
51 | if (h.substring(u, u + 1) != " ") {
52 | for (; h.substring(u, u + 1) != " " && u >= 0; )
53 | u--;
54 | u > 0 && (t = u);
55 | }
56 | }
57 | t === 0 && (t = 1), l = h.substring(0, t), l = p ? B({
58 | ctx: e,
59 | line: l,
60 | spaceWidth: m,
61 | spaceChar: W,
62 | width: n
63 | }) : l, o.push(l), h = h.substring(t), g = i(h);
64 | }
65 | g > 0 && (l = p ? B({
66 | ctx: e,
67 | line: h,
68 | spaceWidth: m,
69 | spaceChar: W,
70 | width: n
71 | }) : h, o.push(l));
72 | }
73 | return o;
74 | }
75 | function H({
76 | ctx: e,
77 | text: c,
78 | style: p
79 | }) {
80 | const n = e.textBaseline, a = e.font;
81 | e.textBaseline = "bottom", e.font = p;
82 | const { actualBoundingBoxAscent: i } = e.measureText(c);
83 | return e.textBaseline = n, e.font = a, i;
84 | }
85 | const C = {
86 | debug: !1,
87 | align: "center",
88 | vAlign: "middle",
89 | fontSize: 14,
90 | fontWeight: "",
91 | fontStyle: "",
92 | fontVariant: "",
93 | font: "Arial",
94 | lineHeight: null,
95 | justify: !1
96 | };
97 | function E(e, c, p) {
98 | const { width: n, height: a, x: i, y: o } = p, s = { ...C, ...p };
99 | if (n <= 0 || a <= 0 || s.fontSize <= 0)
100 | return { height: 0 };
101 | const m = i + n, d = o + a, { fontStyle: b, fontVariant: r, fontWeight: g, fontSize: y, font: h } = s, t = `${b} ${r} ${g} ${y}px ${h}`;
102 | e.font = t;
103 | let f = o + a / 2 + s.fontSize / 2, l;
104 | s.align === "right" ? (l = m, e.textAlign = "right") : s.align === "left" ? (l = i, e.textAlign = "left") : (l = i + n / 2, e.textAlign = "center");
105 | const u = k({
106 | ctx: e,
107 | text: c,
108 | justify: s.justify,
109 | width: n
110 | }), S = s.lineHeight ? s.lineHeight : H({ ctx: e, text: "M", style: t }), v = S * (u.length - 1), P = v / 2;
111 | let A = o;
112 | if (s.vAlign === "top" ? (e.textBaseline = "top", f = o) : s.vAlign === "bottom" ? (e.textBaseline = "bottom", f = d - v, A = d) : (e.textBaseline = "bottom", A = o + a / 2, f -= P), u.forEach((T) => {
113 | T = T.trim(), e.fillText(T, l, f), f += S;
114 | }), s.debug) {
115 | const T = "#0C8CE9";
116 | e.lineWidth = 1, e.strokeStyle = T, e.strokeRect(i, o, n, a), e.lineWidth = 1, e.strokeStyle = T, e.beginPath(), e.moveTo(l, o), e.lineTo(l, d), e.stroke(), e.strokeStyle = T, e.beginPath(), e.moveTo(i, A), e.lineTo(m, A), e.stroke();
117 | }
118 | return { height: v + S };
119 | }
120 | export {
121 | E as drawText,
122 | H as getTextHeight,
123 | k as splitText
124 | };
125 |
--------------------------------------------------------------------------------
/src/uWrap.ts:
--------------------------------------------------------------------------------
1 | // BREAKS
2 | const D = "-".charCodeAt(0);
3 | const S = " ".charCodeAt(0);
4 | const N = "\n".charCodeAt(0);
5 | // const R = "\r".charCodeAt(0); (TODO: support \r\n breaks)
6 | // const T = "\t".charCodeAt(0);
7 |
8 | const SYMBS = `\`~!@#$%^&*()_+-=[]\\{}|;':",./<>? \t`;
9 | const NUMS = "1234567890";
10 | const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
11 | const LOWER = "abcdefghijklmnopqrstuvwxyz";
12 | const CHARS = `${UPPER}${LOWER}${NUMS}${SYMBS}`;
13 |
14 | function supportsLetterSpacing(ctx: CanvasRenderingContext2D) {
15 | const _w = ctx.measureText('W').width;
16 | const _letterSpacing = ctx.letterSpacing;
17 | ctx.letterSpacing = '101px';
18 | const w = ctx.measureText('W').width;
19 | ctx.letterSpacing = _letterSpacing;
20 | return w > _w;
21 | }
22 |
23 | export function varPreLine(ctx: CanvasRenderingContext2D) {
24 | // Safari pre-18.4 does not support Canvas letterSpacing, and measureText() does not account for it
25 | // so we have to add it manually. https://caniuse.com/mdn-api_canvasrenderingcontext2d_letterspacing
26 | const fauxLetterSpacing = !supportsLetterSpacing(ctx) ? parseFloat(ctx.letterSpacing) : 0;
27 |
28 | // single-char widths in isolation
29 | const WIDTHS: Record = {};
30 |
31 | for (let i = 0; i < CHARS.length; i++)
32 | WIDTHS[CHARS.charCodeAt(i)] = ctx.measureText(CHARS[i]).width + fauxLetterSpacing;
33 |
34 | const wordSpacing = parseFloat(ctx.wordSpacing);
35 |
36 | if (wordSpacing > 0)
37 | WIDTHS[S] = wordSpacing;
38 |
39 | // build kerning/spacing LUT of upper+lower, upper+sym, upper+upper pairs. (this includes letterSpacing)
40 | // holds kerning-adjusted width of the uppers
41 | const PAIRS: Record> = {};
42 |
43 | for (let i = 0; i < UPPER.length; i++) {
44 | let uc = UPPER.charCodeAt(i);
45 | PAIRS[uc] = {};
46 |
47 | for (let j = 0; j < CHARS.length; j++) {
48 | let ch = CHARS.charCodeAt(j);
49 | let wid = ctx.measureText(`${UPPER[i]}${CHARS[j]}`).width - WIDTHS[ch] + fauxLetterSpacing;
50 | PAIRS[uc][ch] = wid;
51 | }
52 | }
53 |
54 | type LineCallback = (idx0: number, idx1: number, width: number) => void | boolean;
55 |
56 | const eachLine: LineCallback = () => {};
57 |
58 | function each(text: string, width: number, cb = eachLine) {
59 | let fr = 0;
60 | while (text.charCodeAt(fr) === S)
61 | fr++;
62 |
63 | let to = text.length - 1;
64 | while (text.charCodeAt(to) === S)
65 | to--;
66 |
67 | let headIdx = fr;
68 | let headEnd = 0;
69 | let headWid = 0;
70 |
71 | let tailIdx = -1; // wrap candidate
72 | let tailWid = 0;
73 |
74 | let inWS = false;
75 |
76 | for (let i = fr; i <= to; i++) {
77 | let c = text.charCodeAt(i);
78 |
79 | let w = 0;
80 |
81 | if (c in PAIRS) {
82 | let n = text.charCodeAt(i + 1);
83 |
84 | if (n in PAIRS[c])
85 | w = PAIRS[c][n];
86 | }
87 |
88 | if (w === 0)
89 | w = WIDTHS[c] ?? (WIDTHS[c] = ctx.measureText(text[i]).width);
90 |
91 | if (c === S) { // || c === T || c === N || c === R
92 | // set possible wrap point
93 | if (text.charCodeAt(i + 1) !== c) {
94 | tailIdx = i + 1;
95 | tailWid = 0;
96 | }
97 |
98 | if (!inWS && headWid > 0) {
99 | headWid += w;
100 | headEnd = i;
101 | }
102 |
103 | inWS = true;
104 | }
105 | else if (c === N) {
106 | if (cb(headIdx, i, headWid) === false)
107 | return;
108 |
109 | headIdx = headEnd = i + 1;
110 | headWid = tailWid = 0;
111 | tailWid = 0;
112 | tailIdx = -1;
113 | }
114 | else {
115 | if (headEnd > headIdx && headWid + w > width) {
116 | if (cb(headIdx, headEnd, headWid) === false)
117 | return;
118 |
119 | headWid = tailWid + w;
120 | headIdx = headEnd = tailIdx;
121 | tailWid = 0;
122 | tailIdx = -1;
123 | } else {
124 | if (c === D) {
125 | // set possible wrap point
126 | if (text.charCodeAt(i + 1) !== c) {
127 | tailIdx = headEnd = i + 1;
128 | tailWid = 0;
129 | }
130 | }
131 |
132 | headWid += w;
133 | tailWid += w;
134 | }
135 |
136 | inWS = false;
137 | }
138 | }
139 |
140 | cb(headIdx, to + 1, headWid);
141 | }
142 |
143 | let mayWrap = /\s|-/;
144 |
145 | return {
146 | each,
147 | split: (text: string, width: number, limit = Infinity) => {
148 | let out: string[] = [];
149 |
150 | if (mayWrap.test(text)) {
151 | each(text, width, (idx0: number, idx1: number) => {
152 | out.push(text.slice(idx0, idx1));
153 |
154 | if (out.length === limit)
155 | return false;
156 | });
157 | } else {
158 | out.push(text);
159 | }
160 |
161 | return out;
162 | },
163 | count: (text: string, width: number) => {
164 | let count = 0;
165 |
166 | if (mayWrap.test(text)) {
167 | each(text, width, () => { count++; });
168 | } else {
169 | count = 1;
170 | }
171 |
172 | return count;
173 | },
174 | test: (text: string, width: number) => {
175 | let count = 0;
176 |
177 | if (mayWrap.test(text)) {
178 | each(text, width, () => {
179 | if (++count === 2)
180 | return false;
181 | });
182 | } else {
183 | count = 1;
184 | }
185 |
186 | return count === 2;
187 | },
188 | };
189 | }
190 |
191 | /*
192 | function isMonospace(ctx: CanvasRenderingContext2D) {
193 | let w = ctx.measureText('.').width;
194 | return ctx.measureText('i').width === w && ctx.measureText('.').width === w;
195 | }
196 | */
197 |
--------------------------------------------------------------------------------
/dist/uWrap.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2025, Leon Sorokin
3 | * All rights reserved. (MIT Licensed)
4 | *
5 | * uWrap.js
6 | * A small, fast line wrapping thing for Canvas2D
7 | * https://github.com/leeoniya/uWrap (v0.1.2)
8 | */
9 |
10 | // BREAKS
11 | const D = "-".charCodeAt(0);
12 | const S = " ".charCodeAt(0);
13 | const N = "\n".charCodeAt(0);
14 | // const R = "\r".charCodeAt(0); (TODO: support \r\n breaks)
15 | // const T = "\t".charCodeAt(0);
16 | const SYMBS = `\`~!@#$%^&*()_+-=[]\\{}|;':",./<>? \t`;
17 | const NUMS = "1234567890";
18 | const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
19 | const LOWER = "abcdefghijklmnopqrstuvwxyz";
20 | const CHARS = `${UPPER}${LOWER}${NUMS}${SYMBS}`;
21 | function supportsLetterSpacing(ctx) {
22 | const _w = ctx.measureText('W').width;
23 | const _letterSpacing = ctx.letterSpacing;
24 | ctx.letterSpacing = '101px';
25 | const w = ctx.measureText('W').width;
26 | ctx.letterSpacing = _letterSpacing;
27 | return w > _w;
28 | }
29 | function varPreLine(ctx) {
30 | // Safari pre-18.4 does not support Canvas letterSpacing, and measureText() does not account for it
31 | // so we have to add it manually. https://caniuse.com/mdn-api_canvasrenderingcontext2d_letterspacing
32 | const fauxLetterSpacing = !supportsLetterSpacing(ctx) ? parseFloat(ctx.letterSpacing) : 0;
33 | // single-char widths in isolation
34 | const WIDTHS = {};
35 | for (let i = 0; i < CHARS.length; i++)
36 | WIDTHS[CHARS.charCodeAt(i)] = ctx.measureText(CHARS[i]).width + fauxLetterSpacing;
37 | const wordSpacing = parseFloat(ctx.wordSpacing);
38 | if (wordSpacing > 0)
39 | WIDTHS[S] = wordSpacing;
40 | // build kerning/spacing LUT of upper+lower, upper+sym, upper+upper pairs. (this includes letterSpacing)
41 | // holds kerning-adjusted width of the uppers
42 | const PAIRS = {};
43 | for (let i = 0; i < UPPER.length; i++) {
44 | let uc = UPPER.charCodeAt(i);
45 | PAIRS[uc] = {};
46 | for (let j = 0; j < CHARS.length; j++) {
47 | let ch = CHARS.charCodeAt(j);
48 | let wid = ctx.measureText(`${UPPER[i]}${CHARS[j]}`).width - WIDTHS[ch] + fauxLetterSpacing;
49 | PAIRS[uc][ch] = wid;
50 | }
51 | }
52 | const eachLine = () => { };
53 | function each(text, width, cb = eachLine) {
54 | let fr = 0;
55 | while (text.charCodeAt(fr) === S)
56 | fr++;
57 | let to = text.length - 1;
58 | while (text.charCodeAt(to) === S)
59 | to--;
60 | let headIdx = fr;
61 | let headEnd = 0;
62 | let headWid = 0;
63 | let tailIdx = -1; // wrap candidate
64 | let tailWid = 0;
65 | let inWS = false;
66 | for (let i = fr; i <= to; i++) {
67 | let c = text.charCodeAt(i);
68 | let w = 0;
69 | if (c in PAIRS) {
70 | let n = text.charCodeAt(i + 1);
71 | if (n in PAIRS[c])
72 | w = PAIRS[c][n];
73 | }
74 | if (w === 0)
75 | w = WIDTHS[c] ?? (WIDTHS[c] = ctx.measureText(text[i]).width);
76 | if (c === S) { // || c === T || c === N || c === R
77 | // set possible wrap point
78 | if (text.charCodeAt(i + 1) !== c) {
79 | tailIdx = i + 1;
80 | tailWid = 0;
81 | }
82 | if (!inWS && headWid > 0) {
83 | headWid += w;
84 | headEnd = i;
85 | }
86 | inWS = true;
87 | }
88 | else if (c === N) {
89 | if (cb(headIdx, i, headWid) === false)
90 | return;
91 | headIdx = headEnd = i + 1;
92 | headWid = tailWid = 0;
93 | tailWid = 0;
94 | tailIdx = -1;
95 | }
96 | else {
97 | if (headEnd > headIdx && headWid + w > width) {
98 | if (cb(headIdx, headEnd, headWid) === false)
99 | return;
100 | headWid = tailWid + w;
101 | headIdx = headEnd = tailIdx;
102 | tailWid = 0;
103 | tailIdx = -1;
104 | }
105 | else {
106 | if (c === D) {
107 | // set possible wrap point
108 | if (text.charCodeAt(i + 1) !== c) {
109 | tailIdx = headEnd = i + 1;
110 | tailWid = 0;
111 | }
112 | }
113 | headWid += w;
114 | tailWid += w;
115 | }
116 | inWS = false;
117 | }
118 | }
119 | cb(headIdx, to + 1, headWid);
120 | }
121 | let mayWrap = /\s|-/;
122 | return {
123 | each,
124 | split: (text, width, limit = Infinity) => {
125 | let out = [];
126 | if (mayWrap.test(text)) {
127 | each(text, width, (idx0, idx1) => {
128 | out.push(text.slice(idx0, idx1));
129 | if (out.length === limit)
130 | return false;
131 | });
132 | }
133 | else {
134 | out.push(text);
135 | }
136 | return out;
137 | },
138 | count: (text, width) => {
139 | let count = 0;
140 | if (mayWrap.test(text)) {
141 | each(text, width, () => { count++; });
142 | }
143 | else {
144 | count = 1;
145 | }
146 | return count;
147 | },
148 | test: (text, width) => {
149 | let count = 0;
150 | if (mayWrap.test(text)) {
151 | each(text, width, () => {
152 | if (++count === 2)
153 | return false;
154 | });
155 | }
156 | else {
157 | count = 1;
158 | }
159 | return count === 2;
160 | },
161 | };
162 | }
163 | /*
164 | function isMonospace(ctx: CanvasRenderingContext2D) {
165 | let w = ctx.measureText('.').width;
166 | return ctx.measureText('i').width === w && ctx.measureText('.').width === w;
167 | }
168 | */
169 |
170 | export { varPreLine };
171 |
--------------------------------------------------------------------------------
/dist/uWrap.iife.js:
--------------------------------------------------------------------------------
1 | var uWrap = (function (exports) {
2 | 'use strict';
3 |
4 | /**
5 | * Copyright (c) 2025, Leon Sorokin
6 | * All rights reserved. (MIT Licensed)
7 | *
8 | * uWrap.js
9 | * A small, fast line wrapping thing for Canvas2D
10 | * https://github.com/leeoniya/uWrap (v0.1.2)
11 | */
12 |
13 | // BREAKS
14 | const D = "-".charCodeAt(0);
15 | const S = " ".charCodeAt(0);
16 | const N = "\n".charCodeAt(0);
17 | // const R = "\r".charCodeAt(0); (TODO: support \r\n breaks)
18 | // const T = "\t".charCodeAt(0);
19 | const SYMBS = `\`~!@#$%^&*()_+-=[]\\{}|;':",./<>? \t`;
20 | const NUMS = "1234567890";
21 | const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
22 | const LOWER = "abcdefghijklmnopqrstuvwxyz";
23 | const CHARS = `${UPPER}${LOWER}${NUMS}${SYMBS}`;
24 | function supportsLetterSpacing(ctx) {
25 | const _w = ctx.measureText('W').width;
26 | const _letterSpacing = ctx.letterSpacing;
27 | ctx.letterSpacing = '101px';
28 | const w = ctx.measureText('W').width;
29 | ctx.letterSpacing = _letterSpacing;
30 | return w > _w;
31 | }
32 | function varPreLine(ctx) {
33 | // Safari pre-18.4 does not support Canvas letterSpacing, and measureText() does not account for it
34 | // so we have to add it manually. https://caniuse.com/mdn-api_canvasrenderingcontext2d_letterspacing
35 | const fauxLetterSpacing = !supportsLetterSpacing(ctx) ? parseFloat(ctx.letterSpacing) : 0;
36 | // single-char widths in isolation
37 | const WIDTHS = {};
38 | for (let i = 0; i < CHARS.length; i++)
39 | WIDTHS[CHARS.charCodeAt(i)] = ctx.measureText(CHARS[i]).width + fauxLetterSpacing;
40 | const wordSpacing = parseFloat(ctx.wordSpacing);
41 | if (wordSpacing > 0)
42 | WIDTHS[S] = wordSpacing;
43 | // build kerning/spacing LUT of upper+lower, upper+sym, upper+upper pairs. (this includes letterSpacing)
44 | // holds kerning-adjusted width of the uppers
45 | const PAIRS = {};
46 | for (let i = 0; i < UPPER.length; i++) {
47 | let uc = UPPER.charCodeAt(i);
48 | PAIRS[uc] = {};
49 | for (let j = 0; j < CHARS.length; j++) {
50 | let ch = CHARS.charCodeAt(j);
51 | let wid = ctx.measureText(`${UPPER[i]}${CHARS[j]}`).width - WIDTHS[ch] + fauxLetterSpacing;
52 | PAIRS[uc][ch] = wid;
53 | }
54 | }
55 | const eachLine = () => { };
56 | function each(text, width, cb = eachLine) {
57 | let fr = 0;
58 | while (text.charCodeAt(fr) === S)
59 | fr++;
60 | let to = text.length - 1;
61 | while (text.charCodeAt(to) === S)
62 | to--;
63 | let headIdx = fr;
64 | let headEnd = 0;
65 | let headWid = 0;
66 | let tailIdx = -1; // wrap candidate
67 | let tailWid = 0;
68 | let inWS = false;
69 | for (let i = fr; i <= to; i++) {
70 | let c = text.charCodeAt(i);
71 | let w = 0;
72 | if (c in PAIRS) {
73 | let n = text.charCodeAt(i + 1);
74 | if (n in PAIRS[c])
75 | w = PAIRS[c][n];
76 | }
77 | if (w === 0)
78 | w = WIDTHS[c] ?? (WIDTHS[c] = ctx.measureText(text[i]).width);
79 | if (c === S) { // || c === T || c === N || c === R
80 | // set possible wrap point
81 | if (text.charCodeAt(i + 1) !== c) {
82 | tailIdx = i + 1;
83 | tailWid = 0;
84 | }
85 | if (!inWS && headWid > 0) {
86 | headWid += w;
87 | headEnd = i;
88 | }
89 | inWS = true;
90 | }
91 | else if (c === N) {
92 | if (cb(headIdx, i, headWid) === false)
93 | return;
94 | headIdx = headEnd = i + 1;
95 | headWid = tailWid = 0;
96 | tailWid = 0;
97 | tailIdx = -1;
98 | }
99 | else {
100 | if (headEnd > headIdx && headWid + w > width) {
101 | if (cb(headIdx, headEnd, headWid) === false)
102 | return;
103 | headWid = tailWid + w;
104 | headIdx = headEnd = tailIdx;
105 | tailWid = 0;
106 | tailIdx = -1;
107 | }
108 | else {
109 | if (c === D) {
110 | // set possible wrap point
111 | if (text.charCodeAt(i + 1) !== c) {
112 | tailIdx = headEnd = i + 1;
113 | tailWid = 0;
114 | }
115 | }
116 | headWid += w;
117 | tailWid += w;
118 | }
119 | inWS = false;
120 | }
121 | }
122 | cb(headIdx, to + 1, headWid);
123 | }
124 | let mayWrap = /\s|-/;
125 | return {
126 | each,
127 | split: (text, width, limit = Infinity) => {
128 | let out = [];
129 | if (mayWrap.test(text)) {
130 | each(text, width, (idx0, idx1) => {
131 | out.push(text.slice(idx0, idx1));
132 | if (out.length === limit)
133 | return false;
134 | });
135 | }
136 | else {
137 | out.push(text);
138 | }
139 | return out;
140 | },
141 | count: (text, width) => {
142 | let count = 0;
143 | if (mayWrap.test(text)) {
144 | each(text, width, () => { count++; });
145 | }
146 | else {
147 | count = 1;
148 | }
149 | return count;
150 | },
151 | test: (text, width) => {
152 | let count = 0;
153 | if (mayWrap.test(text)) {
154 | each(text, width, () => {
155 | if (++count === 2)
156 | return false;
157 | });
158 | }
159 | else {
160 | count = 1;
161 | }
162 | return count === 2;
163 | },
164 | };
165 | }
166 |
167 | exports.varPreLine = varPreLine;
168 |
169 | return exports;
170 |
171 | })({});
172 |
--------------------------------------------------------------------------------
/demo/canvas-hypertxt.mjs:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // src/multi-line.ts
4 | var resultCache = /* @__PURE__ */ new Map();
5 | var metrics = /* @__PURE__ */ new Map();
6 | var hyperMaps = /* @__PURE__ */ new Map();
7 | function clearMultilineCache() {
8 | resultCache.clear();
9 | hyperMaps.clear();
10 | metrics.clear();
11 | }
12 | function backProp(text, realWidth, keyMap, temperature, avgSize) {
13 | var _a, _b, _c;
14 | let guessWidth = 0;
15 | const contribMap = {};
16 | for (const char of text) {
17 | const v = (_a = keyMap.get(char)) != null ? _a : avgSize;
18 | guessWidth += v;
19 | contribMap[char] = ((_b = contribMap[char]) != null ? _b : 0) + 1;
20 | ``;
21 | }
22 | const diff = realWidth - guessWidth;
23 | for (const key of Object.keys(contribMap)) {
24 | const numContribution = contribMap[key];
25 | const contribWidth = (_c = keyMap.get(key)) != null ? _c : avgSize;
26 | const contribAmount = contribWidth * numContribution / guessWidth;
27 | const adjustment = diff * contribAmount * temperature / numContribution;
28 | const newVal = contribWidth + adjustment;
29 | keyMap.set(key, newVal);
30 | }
31 | }
32 | function makeHyperMap(ctx, avgSize) {
33 | var _a;
34 | const result = /* @__PURE__ */ new Map();
35 | let total = 0;
36 | for (const char of "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890,.-+=?") {
37 | const w = ctx.measureText(char).width;
38 | result.set(char, w);
39 | total += w;
40 | }
41 | const avg = total / result.size;
42 | const damper = 3;
43 | const scaler = (avgSize / avg + damper) / (damper + 1);
44 | const keys = result.keys();
45 | for (const key of keys) {
46 | result.set(key, ((_a = result.get(key)) != null ? _a : avg) * scaler);
47 | }
48 | return result;
49 | }
50 | function measureText(ctx, text, fontStyle, hyperMode) {
51 | var _a, _b;
52 | const current = metrics.get(fontStyle);
53 | if (hyperMode && current !== void 0 && current.count > 2e4) {
54 | let hyperMap = hyperMaps.get(fontStyle);
55 | if (hyperMap === void 0) {
56 | hyperMap = makeHyperMap(ctx, current.size);
57 | hyperMaps.set(fontStyle, hyperMap);
58 | }
59 | if (current.count > 5e5) {
60 | let final = 0;
61 | for (const char of text) {
62 | final += (_a = hyperMap.get(char)) != null ? _a : current.size;
63 | }
64 | return final * 1.01;
65 | }
66 | const result2 = ctx.measureText(text);
67 | backProp(text, result2.width, hyperMap, Math.max(0.05, 1 - current.count / 2e5), current.size);
68 | metrics.set(fontStyle, {
69 | count: current.count + text.length,
70 | size: current.size
71 | });
72 | return result2.width;
73 | }
74 | const result = ctx.measureText(text);
75 | const avg = result.width / text.length;
76 | if (((_b = current == null ? void 0 : current.count) != null ? _b : 0) > 2e4) {
77 | return result.width;
78 | }
79 | if (current === void 0) {
80 | metrics.set(fontStyle, {
81 | count: text.length,
82 | size: avg
83 | });
84 | } else {
85 | const diff = avg - current.size;
86 | const contribution = text.length / (current.count + text.length);
87 | const newVal = current.size + diff * contribution;
88 | metrics.set(fontStyle, {
89 | count: current.count + text.length,
90 | size: newVal
91 | });
92 | }
93 | return result.width;
94 | }
95 | function getSplitPoint(ctx, text, width, fontStyle, totalWidth, measuredChars, hyperMode, getBreakOpportunities) {
96 | if (text.length <= 1)
97 | return text.length;
98 | if (totalWidth < width)
99 | return -1;
100 | let guess = Math.floor(width / totalWidth * measuredChars);
101 | let guessWidth = measureText(ctx, text.slice(0, Math.max(0, guess)), fontStyle, hyperMode);
102 | const oppos = getBreakOpportunities == null ? void 0 : getBreakOpportunities(text);
103 | if (guessWidth === width) {
104 | } else if (guessWidth < width) {
105 | while (guessWidth < width) {
106 | guess++;
107 | guessWidth = measureText(ctx, text.slice(0, Math.max(0, guess)), fontStyle, hyperMode);
108 | }
109 | guess--;
110 | } else {
111 | while (guessWidth > width) {
112 | const lastSpace = oppos !== void 0 ? 0 : text.lastIndexOf(" ", guess - 1);
113 | if (lastSpace > 0) {
114 | guess = lastSpace;
115 | } else {
116 | guess--;
117 | }
118 | guessWidth = measureText(ctx, text.slice(0, Math.max(0, guess)), fontStyle, hyperMode);
119 | }
120 | }
121 | if (text[guess] !== " ") {
122 | let greedyBreak = 0;
123 | if (oppos === void 0) {
124 | greedyBreak = text.lastIndexOf(" ", guess);
125 | } else {
126 | for (const o of oppos) {
127 | if (o > guess)
128 | break;
129 | greedyBreak = o;
130 | }
131 | }
132 | if (greedyBreak > 0) {
133 | guess = greedyBreak;
134 | }
135 | }
136 | return guess;
137 | }
138 | function splitMultilineText(ctx, value, fontStyle, width, hyperWrappingAllowed, getBreakOpportunities) {
139 | const key = `${value}_${fontStyle}_${width}px`;
140 | const cacheResult = resultCache.get(key);
141 | if (cacheResult !== void 0)
142 | return cacheResult;
143 | if (width <= 0) {
144 | return [];
145 | }
146 | let result = [];
147 | const encodedLines = value.split("\n");
148 | const fontMetrics = metrics.get(fontStyle);
149 | const safeLineGuess = fontMetrics === void 0 ? value.length : width / fontMetrics.size * 1.5;
150 | const hyperMode = hyperWrappingAllowed && fontMetrics !== void 0 && fontMetrics.count > 2e4;
151 | for (let line of encodedLines) {
152 | let textWidth = measureText(ctx, line.slice(0, Math.max(0, safeLineGuess)), fontStyle, hyperMode);
153 | let measuredChars = Math.min(line.length, safeLineGuess);
154 | if (textWidth <= width) {
155 | result.push(line);
156 | } else {
157 | while (textWidth > width) {
158 | const splitPoint = getSplitPoint(ctx, line, width, fontStyle, textWidth, measuredChars, hyperMode, getBreakOpportunities);
159 | const subLine = line.slice(0, Math.max(0, splitPoint));
160 | line = line.slice(subLine.length);
161 | result.push(subLine);
162 | textWidth = measureText(ctx, line.slice(0, Math.max(0, safeLineGuess)), fontStyle, hyperMode);
163 | measuredChars = Math.min(line.length, safeLineGuess);
164 | }
165 | if (textWidth > 0) {
166 | result.push(line);
167 | }
168 | }
169 | }
170 | result = result.map((l, i) => i === 0 ? l.trimEnd() : l.trim());
171 | resultCache.set(key, result);
172 | if (resultCache.size > 500) {
173 | resultCache.delete(resultCache.keys().next().value);
174 | }
175 | return result;
176 | }
177 | export {
178 | clearMultilineCache as clearCache,
179 | splitMultilineText as split
180 | };
181 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | uWrap Demo
5 |
6 |
7 |
8 |
52 |
53 |
54 |
189 |
190 |
The quick brown fox jumps over the lazy dog.
191 |
Rodney Dangerfield, Keith Gordon, Sally Kellerman, Robert Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
192 |
193 |
Rodney Dangerfield, Keith Gordon, Sally Kellerman, Robert Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
194 |
195 |
Bugs Bunny's Third Movie: 1001 Rabbit Tales
196 |
Rodney Dangerfield, Keith Gordon, Sally Kellerman, Robert Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
197 |
Rodney-Dangerfield, Keith Gordon, Sally Kellerman, Robert-Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
198 |
Rodney-Dangerfield, Keith Gordon, Sally Kellerman, Robert3-Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
199 |
200 |
Rodney Dangerfield, Keith Gordon, Sally Kellerman, Robert Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
201 |
202 |
Rodney Dangerfield, Keith Gordon, Sally Kellerman, Robert Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
203 |
204 |
Rodney Dangerfield, Keith Gordon, Sally Kellerman, Robert Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
205 |
206 |
Rodney Dangerfield, Keith Gordon, Sally Kellerman, Robert Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
207 |
Rodney Dangerfield, Keith Gordon, Sally Kellerman, Robert-Downey jr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau
208 |
RodneyDangerfield,KeithGordon,SallyKellerman,RobertDowneyjr., Burt Young, Ned Beatty, Terry Farrell, Paxton Whitehead, M. Emmet Walsh, Adrienne Barbeau