Skip to content

Commit e2dc6b1

Browse files
committed
maybe 0.1.0
1 parent 093cd12 commit e2dc6b1

9 files changed

Lines changed: 205 additions & 58 deletions

File tree

README.md

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
## ⏎ μWrap
22

3-
A [10x faster](#performance) and more accurate text wrapping util in [1.5KB (min)](https://github.com/leeoniya/uWrap/blob/main/dist/uWrap.iife.min.js) _(MIT Licensed)_
3+
A [10x faster](#performance) and more accurate text wrapping util in [< 2KB (min)](https://github.com/leeoniya/uWrap/blob/main/dist/uWrap.iife.min.js) _(MIT Licensed)_
44

55
---
66
### Introduction
77

8-
uWrap was made to efficiently predict varying row heights for list and grid [virtualization](https://www.patterns.dev/vanilla/virtual-lists/) - a strategy for performance optimization when rendering large datasets.
9-
Doing this both quickly and accurately turns out to be a non-trivial task, since Canvas2D provides no API for text wrapping, and `measureText()` is very expensive;
10-
rendering to the DOM is also out of the question due to poor performance.
11-
Additionally, font size, variable-width fonts, `letter-spacing`, explicit line breaks, and different `white-space` choices impact the exact number of lines in final result.
8+
uWrap exists to efficiently predict varying row heights for list and grid [virtualization](https://www.patterns.dev/vanilla/virtual-lists/), a technique for UI performance optimization when rendering large, scrollable datasets.
9+
Doing this both quickly and accurately turns out to be a non-trivial task since Canvas2D provides no API for text wrapping, and `measureText()` is quite expensive;
10+
measuring via DOM is also a non-starter due to poor performance.
11+
Additionally, font size, variable-width fonts, `letter-spacing`, explicit line breaks, and different `white-space` choices affect the number of wrapped lines.
12+
13+
Notes:
14+
15+
- Today, works most accurately with Latin charsets
16+
- Does not yet handle Windows-style `\r\n` explicit line breaks
17+
- Only `pre-line` wrapping strategy is implemented so far
1218

1319
---
1420
### Performance
1521

16-
[canvas-hypertxt](https://github.com/glideapps/canvas-hypertxt) looks to be the fastest similar utility.
17-
uWrap handily out-performs it in both CPU and memory usage by a wide margin while being significantly _more_ accurate.
22+
uWrap handily out-performs [canvas-hypertxt](https://github.com/glideapps/canvas-hypertxt) in both CPU and memory usage by a wide margin while being significantly _more_ accurate.
1823

19-
The benchmark below wraps 100k random sentances within boxes of random widths between 50px and 250px.
20-
You can see this benchmark performed in the console on the [demo page](https://leeoniya.github.io/uWrap/demo/).
24+
The benchmark below wraps 100k random sentances in boxes of random widths between 50px and 250px.
25+
You can see this live in DevTools console of the [demo page](https://leeoniya.github.io/uWrap/demo/).
2126

2227
<table>
2328
<tr>
@@ -70,18 +75,18 @@ let ctx = document.createElement('canvas').getContext('2d');
7075
ctx.font = "14px Inter, sans-serif";
7176
ctx.letterSpacing = '0.15px';
7277

73-
// init wrapper
74-
let wrap = varPreLine(ctx);
78+
// init util fns
79+
const { count, test, split } = varPreLine(ctx);
7580

76-
// sample text
81+
// example text
7782
let text = 'The quick brown fox jumps over the lazy dog.';
7883

79-
// call wrapper with text to wrap, width of bounding container, and per-line callback
80-
let lineCount = 0;
81-
wrap(text, width, () => { count++; });
84+
// count lines
85+
let numLines = count(text, width);
8286

83-
// or process the lines for rendering, etc..
87+
// test if text will wrap
88+
let willWrap = test(text, width);
8489

85-
let lines = [];
86-
wrap(text, width, (idx0, idx1) => { lines.push(text.slice(idx0, idx1)); });
90+
// split lines (with optional limit)
91+
let lines = split(text, width, 3);
8792
```

demo/index.html

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,11 @@
8787

8888
setTimeout(() => {
8989
console.time('uWrap 100k');
90-
let wrap = varPreLine(ctx);
90+
let { count } = varPreLine(ctx);
9191

9292
for (let i = 0; i < strings.length; i++) {
9393
let text = strings[i];
94-
95-
let count = 0;
96-
wrap(text, randInt(50, 250), (idx0, idx1) => { count++; });
97-
98-
// let lines = [];
99-
// wrap(text, width, (idx0, idx1) => { lines.push(text.slice(idx0, idx1)); });
94+
let c = count(text, randInt(50, 250));
10095
}
10196
console.timeEnd('uWrap 100k');
10297
}, 500);
@@ -120,15 +115,15 @@
120115
// ACCURACY OVERLAYS //
121116

122117
setTimeout(() => {
123-
const wrap = varPreLine(ctx);
118+
const { each } = varPreLine(ctx);
124119

125120
for (const el of document.querySelectorAll('.test')) {
126121
let text = el.textContent;
127122
let res = document.createElement('div');
128123
res.className = 'result';
129124

130125
let t = [];
131-
wrap(text, el.offsetWidth, (idx0, idx1) => { t.push(text.slice(idx0, idx1)); });
126+
each(text, el.offsetWidth, (idx0, idx1) => { t.push(text.slice(idx0, idx1)); });
132127
res.textContent = t.join('\n');
133128
el.prepend(res);
134129
}
@@ -144,7 +139,6 @@
144139
// }
145140
// }, 2000);
146141

147-
148142
// setTimeout(() => {
149143
// for (const el of document.querySelectorAll('.test')) {
150144
// let text = el.textContent;

dist/uWrap.d.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1-
/** may return false to halt wrapping */
1+
/** may return false to exit loop early */
22
export type LineCallback = (idx0: number, idx1: number) => void | boolean;
33

4-
export type Wrapper = (text: string, width: number, cb: LineCallback) => void;
4+
/** invoke callback for each line with start/end idxs */
5+
export type Each = (text: string, width: number, cb: LineCallback) => void;
56

7+
/** split into lines array */
8+
export type Split = (text: string, width: number, limit?: number) => string[];
9+
10+
/** count lines */
11+
export type Count = (text: string, width: number) => number;
12+
13+
/** test whether text will wrap (line count > 1) */
14+
export type Test = (text: string, width: number) => boolean;
15+
16+
export interface uWrap {
17+
each: Each;
18+
split: Split;
19+
count: Count;
20+
test: Test;
21+
};
22+
23+
/** wrap text with a variable width font using pre-line whitespace strategy */
624
export function varPreLine(ctx: CanvasRenderingContext2D): Wrapper;

dist/uWrap.iife.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,31 @@ var uWrap = (function (exports) {
121121
}
122122
cb(headIdx, to + 1);
123123
}
124-
return each;
124+
return {
125+
each,
126+
split: (text, width, limit = Infinity) => {
127+
let out = [];
128+
each(text, width, (idx0, idx1) => {
129+
out.push(text.slice(idx0, idx1));
130+
if (out.length === limit)
131+
return false;
132+
});
133+
return out;
134+
},
135+
count: (text, width) => {
136+
let count = 0;
137+
each(text, width, () => { count++; });
138+
return count;
139+
},
140+
test: (text, width) => {
141+
let count = 0;
142+
each(text, width, () => {
143+
if (++count === 2)
144+
return false;
145+
});
146+
return count === 2;
147+
},
148+
};
125149
}
126150

127151
exports.varPreLine = varPreLine;

dist/uWrap.iife.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/uWrap.mjs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,31 @@ function varPreLine(ctx) {
118118
}
119119
cb(headIdx, to + 1);
120120
}
121-
return each;
121+
return {
122+
each,
123+
split: (text, width, limit = Infinity) => {
124+
let out = [];
125+
each(text, width, (idx0, idx1) => {
126+
out.push(text.slice(idx0, idx1));
127+
if (out.length === limit)
128+
return false;
129+
});
130+
return out;
131+
},
132+
count: (text, width) => {
133+
let count = 0;
134+
each(text, width, () => { count++; });
135+
return count;
136+
},
137+
test: (text, width) => {
138+
let count = 0;
139+
each(text, width, () => {
140+
if (++count === 2)
141+
return false;
142+
});
143+
return count === 2;
144+
},
145+
};
122146
}
123147
/*
124148
function isMonospace(ctx: CanvasRenderingContext2D) {

package.json

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "uwrap",
33
"version": "0.1.0",
4-
"description": "",
4+
"description": "A very fast and accurate text and line wrapping util",
55
"homepage": "https://github.com/leeoniya/uWrap#readme",
66
"bugs": {
77
"url": "https://github.com/leeoniya/uWrap/issues"
@@ -10,19 +10,37 @@
1010
"type": "git",
1111
"url": "git+https://github.com/leeoniya/uWrap.git"
1212
},
13-
"license": "ISC",
13+
"license": "MIT",
1414
"author": "",
1515
"type": "module",
16-
"module": "./dist/uDSV.mjs",
17-
"types": "./dist/uDSV.d.ts",
16+
"module": "./dist/uWrap.mjs",
17+
"types": "./dist/uWrap.d.ts",
18+
"files": [
19+
"package.json",
20+
"README.md",
21+
"LICENSE",
22+
"dist"
23+
],
24+
"keywords": [
25+
"text",
26+
"line",
27+
"word",
28+
"wrap",
29+
"wrapping",
30+
"split",
31+
"virtualization",
32+
"virtualize",
33+
"canvas",
34+
"measure"
35+
],
1836
"scripts": {
1937
"test": "node ./test/uWrap.test.mjs",
2038
"build": "rollup -c"
2139
},
2240
"devDependencies": {
2341
"@rollup/plugin-terser": "^0.4.4",
2442
"@rollup/plugin-typescript": "^12.1.2",
25-
"rollup": "^4.37.0",
43+
"rollup": "^4.39.0",
2644
"skia-canvas": "^2.0.2",
2745
"tslib": "^2.8.1"
2846
}

src/uWrap.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,32 @@ export function varPreLine(ctx: CanvasRenderingContext2D) {
140140
cb(headIdx, to + 1);
141141
}
142142

143-
return each;
143+
return {
144+
each,
145+
split: (text: string, width: number, limit = Infinity) => {
146+
let out: string[] = [];
147+
each(text, width, (idx0: number, idx1: number) => {
148+
out.push(text.slice(idx0, idx1));
149+
150+
if (out.length === limit)
151+
return false;
152+
});
153+
return out;
154+
},
155+
count: (text: string, width: number) => {
156+
let count = 0;
157+
each(text, width, () => { count++; });
158+
return count;
159+
},
160+
test: (text: string, width: number) => {
161+
let count = 0;
162+
each(text, width, () => {
163+
if (++count === 2)
164+
return false;
165+
});
166+
return count === 2;
167+
},
168+
};
144169
}
145170

146171
/*

0 commit comments

Comments
 (0)