ntk 8.5.0 → 8.6.0
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/lib/index.js +17 -0
- package/lib/picture.js +25 -0
- package/lib/shadow.js +36 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -48,6 +48,13 @@ import {
|
|
|
48
48
|
setDefaultRasterizer
|
|
49
49
|
} from './rasterize.js';
|
|
50
50
|
import { DEFAULT_MASK_POLICY } from './maskcluster.js';
|
|
51
|
+
import {
|
|
52
|
+
DEFAULT_SHADOW_POLICY,
|
|
53
|
+
blurCoverage,
|
|
54
|
+
gaussianKernel1d,
|
|
55
|
+
shadowReach,
|
|
56
|
+
shadowSigma
|
|
57
|
+
} from './shadow.js';
|
|
51
58
|
import { DEFAULT_SHAPE_POLICY } from './shapeglyphs.js';
|
|
52
59
|
import { DEFAULT_SHARED_GLYPHS_POLICY } from './glyphdirectory.js';
|
|
53
60
|
import { warmSharedGlyphs } from './text/glyphs.js';
|
|
@@ -275,6 +282,16 @@ export {
|
|
|
275
282
|
DEFAULT_RASTER_POLICY,
|
|
276
283
|
DEFAULT_MASK_POLICY,
|
|
277
284
|
DEFAULT_SHAPE_POLICY,
|
|
285
|
+
// the shadow blur as a primitive (docs/surface.md#baking-a-blur), for a
|
|
286
|
+
// caller drawing shapes ntk's shadow properties never see. `blurCoverage`
|
|
287
|
+
// *bakes* the two separable passes into a surface's pixels — unlike
|
|
288
|
+
// `picture().setBlurFilter()`, whose kernel the server re-runs on every
|
|
289
|
+
// composite, which a cached shadow pays for once per frame
|
|
290
|
+
DEFAULT_SHADOW_POLICY,
|
|
291
|
+
blurCoverage,
|
|
292
|
+
gaussianKernel1d,
|
|
293
|
+
shadowReach,
|
|
294
|
+
shadowSigma,
|
|
278
295
|
// cross-process shared glyphs (docs/shared-glyphs.md): the directory-side
|
|
279
296
|
// budget, and the prewarm that makes a first paint of already-shared text
|
|
280
297
|
// rasterize and upload nothing
|
package/lib/picture.js
CHANGED
|
@@ -25,10 +25,35 @@ export default class Picture {
|
|
|
25
25
|
registry.register(this, { Render: this.Render, X, id: this.id }, this);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Set the picture's filter — a *property of the picture*, not an operation
|
|
30
|
+
* on its pixels. The server re-applies it every time the picture is
|
|
31
|
+
* sampled, so the cost is per composite and forever, not once.
|
|
32
|
+
*
|
|
33
|
+
* That is what makes it right for resampling (`'bilinear'` under a
|
|
34
|
+
* transform) and a trap for anything expensive: see `setBlurFilter`.
|
|
35
|
+
*/
|
|
28
36
|
setFilter(name, params) {
|
|
29
37
|
this.Render.SetPictureFilter(this.id, name, params);
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Hang a k×k gaussian `convolution` on the picture.
|
|
42
|
+
*
|
|
43
|
+
* **This re-convolves on every composite** — it is a filter, so the server
|
|
44
|
+
* runs the whole kernel each time the picture is drawn, and the pixels
|
|
45
|
+
* never change on the client's side of the wire. A picture blurred once and
|
|
46
|
+
* then composited each frame pays k² multiply-accumulates per pixel per
|
|
47
|
+
* frame: at radius 61 over 489×134 that is 244M per draw, which is a 1.6s
|
|
48
|
+
* hover on XQuartz and a 9s window repaint (issue #335).
|
|
49
|
+
*
|
|
50
|
+
* Reach for it when the blur really is per-draw and small. To blur
|
|
51
|
+
* something *once* and composite the result cheaply afterwards — a drop
|
|
52
|
+
* shadow, a cached soft edge — bake it instead with `blurCoverage` from
|
|
53
|
+
* ntk's entry point: two separable 1d passes (2k multiplies per pixel, not
|
|
54
|
+
* k²), run once, leaving a surface with the blur in its pixels and no
|
|
55
|
+
* filter of its own. See docs/surface.md#baking-a-blur.
|
|
56
|
+
*/
|
|
32
57
|
setBlurFilter(radius, sigma) {
|
|
33
58
|
if (radius === 0) {
|
|
34
59
|
return this.setFilter('convolution', [1, 1, 1]);
|
package/lib/shadow.js
CHANGED
|
@@ -32,6 +32,19 @@
|
|
|
32
32
|
// second pass leaves a surface with the blur already in its pixels, so the
|
|
33
33
|
// cached copy composites as a plain mask rather than re-running a kernel on
|
|
34
34
|
// every frame.
|
|
35
|
+
//
|
|
36
|
+
// ## What of this is public
|
|
37
|
+
//
|
|
38
|
+
// The bake and the maths around it — `blurCoverage`, `shadowSigma`,
|
|
39
|
+
// `shadowReach`, `gaussianKernel1d` and `DEFAULT_SHADOW_POLICY` — are
|
|
40
|
+
// re-exported from `lib/index.js` (issue #335). A toolkit that draws its own
|
|
41
|
+
// shapes (react-x11 paints a `<box>`'s `boxShadow` itself, because the
|
|
42
|
+
// rounded rect is a path it already has and the result goes through its own
|
|
43
|
+
// paint cache) needs exactly this and nothing else: the alternative on the
|
|
44
|
+
// public surface, `picture().setBlurFilter()`, sets a k×k filter that the
|
|
45
|
+
// server re-runs on *every* composite, which is invisible until someone
|
|
46
|
+
// profiles a real display. The surfaces and the cache below stay private —
|
|
47
|
+
// they are the 2d context's bookkeeping, not a primitive.
|
|
35
48
|
import { Surface } from './surface.js';
|
|
36
49
|
|
|
37
50
|
/**
|
|
@@ -117,8 +130,31 @@ export function gaussianKernel1d(sigma, reach = shadowReach(sigma)) {
|
|
|
117
130
|
* and leaving it alive would double what the cache is holding. The output
|
|
118
131
|
* carries no filter of its own, so compositing it is an ordinary masked
|
|
119
132
|
* composite no matter how wide the blur was.
|
|
133
|
+
*
|
|
134
|
+
* Public, and the reason is that half of it: a *filter* is re-applied by the
|
|
135
|
+
* server on every composite, so a caller who blurs a picture with
|
|
136
|
+
* `setBlurFilter` and then caches it re-runs the kernel per frame — 244M
|
|
137
|
+
* multiply-accumulates for one 489×134 shadow at σ 10 (issue #335). This
|
|
138
|
+
* bakes instead. `shape` must be a coverage surface with the blur's reach as
|
|
139
|
+
* padding on all four sides, or the result ends in a straight line where the
|
|
140
|
+
* kernel ran out of pixels; see docs/surface.md.
|
|
120
141
|
*/
|
|
121
142
|
export function blurCoverage(shape, sigma) {
|
|
143
|
+
if (!(sigma > 0) || !Number.isFinite(sigma)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`blurCoverage: sigma must be a finite number above 0, got ${sigma}. ` +
|
|
146
|
+
'A canvas blur radius is a diameter, not a sigma — pass ' +
|
|
147
|
+
'shadowSigma(blur), which halves it and applies the policy cap. ' +
|
|
148
|
+
'σ = 0 is no blur at all: composite the coverage as it is.'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (shape.format !== 'a8') {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`blurCoverage: needs a coverage surface, got format ${JSON.stringify(shape.format)}. ` +
|
|
154
|
+
"Draw the shape into new Surface(app, { width, height, format: 'a8' }) — " +
|
|
155
|
+
'the blur runs on alpha, so an argb32 surface would lose its colour.'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
122
158
|
const app = shape.app;
|
|
123
159
|
const R = app.display.Render;
|
|
124
160
|
const { width, height } = shape;
|