ntk 3.9.0 → 3.10.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/color.js ADDED
@@ -0,0 +1,86 @@
1
+ // Colour parsing, shared by the 2d context and the CSS cascade.
2
+ //
3
+ // Everything here returns **premultiplied** `[r, g, b, a]` floats in 0..1,
4
+ // because that is what XRender takes: a colour reaches the server through
5
+ // CreateSolidFill / FillRectangles / gradient stops, all of which read
6
+ // premultiplied ARGB, so each of r, g and b must be <= a. Straight (a.k.a.
7
+ // unassociated) alpha renders at full brightness instead — red at half alpha
8
+ // composites as #ff0000 rather than #800000 — and since x11 3.3.0 a
9
+ // component above 1 also warns.
10
+ //
11
+ // Hex is parsed here rather than handed to parse-color, which does not
12
+ // understand CSS hex alpha and fails silently on it:
13
+ //
14
+ // parse-color('#00000022') -> rgba [0, 0, 0, 34, 1] five entries, and
15
+ // the alpha is still a 0..255 byte
16
+ // parse-color('#0002') -> rgba [0, 2, 0, 1] read as a truncated
17
+ // six-digit hex
18
+ //
19
+ // The first is why `#RRGGBBAA` used to render fully opaque: 34 clamps to 1.
20
+ import parseColorRaw from 'parse-color';
21
+
22
+ /** `[r, g, b, a]` straight -> premultiplied. Opaque colours are unchanged. */
23
+ export function premultiply([r, g, b, a]) {
24
+ return a === 1 ? [r, g, b, a] : [r * a, g * a, b * a, a];
25
+ }
26
+
27
+ // #RGB, #RGBA, #RRGGBB, #RRGGBBAA. Five and seven digits are not CSS, so
28
+ // they are rejected rather than guessed at.
29
+ const HEX = /^#([0-9a-f]{3,8})$/i;
30
+
31
+ function parseHex(value) {
32
+ const m = HEX.exec(value);
33
+ if (!m) return null;
34
+ const h = m[1];
35
+ const short = h.length === 3 || h.length === 4;
36
+ if (!short && h.length !== 6 && h.length !== 8) return null;
37
+ const part = (i) =>
38
+ short
39
+ ? parseInt(h[i], 16) * 0x11 // 'f' -> 0xff
40
+ : parseInt(h.slice(i * 2, i * 2 + 2), 16);
41
+ const hasAlpha = h.length === 4 || h.length === 8;
42
+ return [part(0) / 255, part(1) / 255, part(2) / 255, hasAlpha ? part(3) / 255 : 1];
43
+ }
44
+
45
+ /**
46
+ * Parse a CSS colour to `[r, g, b, a]` floats in 0..1 with **straight**
47
+ * (unassociated) alpha, or null if it is not a colour.
48
+ *
49
+ * `cssColor` is the one to use for anything heading to XRender. This is for
50
+ * the places that genuinely want unassociated components:
51
+ *
52
+ * - **OpenGL.** `glClearColor` and material colours take straight alpha;
53
+ * handing them premultiplied values renders translucent colours dark.
54
+ * - **Interpolating** two colours. Lerp straight, then premultiply once at
55
+ * the end — a round trip back to an `rgba()` string only closes if the
56
+ * components were never scaled.
57
+ */
58
+ export function cssColorStraight(value) {
59
+ if (typeof value !== 'string') return null;
60
+ const v = value.trim();
61
+ if (!v) return null;
62
+ if (v.toLowerCase() === 'transparent') return [0, 0, 0, 0];
63
+
64
+ // '#' is ours alone: falling back to parse-color for a hex form we reject
65
+ // is how '#1234567' turns into rgba [18, 52, 86, 7, 1] -> alpha 7.
66
+ const rgba = v.startsWith('#') ? parseHex(v) : rawRgba(v);
67
+ if (!rgba) return null;
68
+ // NaN would otherwise reach the wire as a garbage fixed-point value
69
+ return rgba.every((c) => Number.isFinite(c)) ? rgba : null;
70
+ }
71
+
72
+ function rawRgba(value) {
73
+ const c = parseColorRaw(value);
74
+ if (!c || !c.rgba) return null;
75
+ const [r, g, b, a] = c.rgba;
76
+ return [r / 255, g / 255, b / 255, a];
77
+ }
78
+
79
+ /**
80
+ * Parse a CSS colour to a premultiplied `[r, g, b, a]` in 0..1, ready for
81
+ * XRender, or null if `value` is not a colour.
82
+ */
83
+ export function cssColor(value) {
84
+ const straight = cssColorStraight(value);
85
+ return straight && premultiply(straight);
86
+ }
package/lib/index.js CHANGED
@@ -22,6 +22,7 @@ import SvgView from './widgets/svgview.js';
22
22
  import MarkdownView from './widgets/markdownview.js';
23
23
  import TexView, { configureTex, layoutTex, TexBox } from './widgets/tex.js';
24
24
  import { tokenize as highlightCode } from './widgets/highlight.js';
25
+ import { cssColorStraight, premultiply } from './color.js';
25
26
  import { cssColor, cssLength } from './widgets/css.js';
26
27
 
27
28
  // rendering context modules register themselves on Drawable
@@ -64,6 +65,12 @@ export function createClient(options, callback) {
64
65
  // preload Render and GLX; GLX is optional ( indirect GLX is often
65
66
  // disabled on modern servers )
66
67
  display.Render = Render;
68
+ // x11 >= 3.3.0 clamps an out-of-range colour component and warns
69
+ // once per connection; under NTK_STRICT_COLORS it throws instead,
70
+ // which is how `npm test` catches a colour that reaches XRender
71
+ // unpremultiplied (r, g or b above alpha) rather than silently
72
+ // rendering too bright.
73
+ if (process.env.NTK_STRICT_COLORS) Render.strictColors = true;
67
74
  display.GLX = glxError ? null : GLX;
68
75
  const X = display.client;
69
76
  X.keycode2keysyms = {};
@@ -129,6 +136,8 @@ export {
129
136
  configureTex,
130
137
  highlightCode,
131
138
  cssColor,
139
+ cssColorStraight,
140
+ premultiply,
132
141
  cssLength
133
142
  };
134
143
  // the yoga-layout instance ntk lays HtmlView out with — downstream layout
@@ -1,7 +1,7 @@
1
1
  import parseFontStyle from 'canvas-fontstyle';
2
2
  import extrudePolyline from 'extrude-polyline';
3
- import parseColorRaw from 'parse-color';
4
3
 
4
+ import { cssColor } from './color.js';
5
5
  import Drawable from './drawable.js';
6
6
  import { Image } from './image.js';
7
7
  import {
@@ -121,10 +121,15 @@ function dashPolyline(pts, closed, pattern, offset) {
121
121
  return { runs, closedLoop: false };
122
122
  }
123
123
 
124
+ // An array is taken as already-premultiplied `[r, g, b, a]` in 0..1 (the
125
+ // documented form in docs/context-2d.md), so it passes through untouched; a
126
+ // string is a CSS colour and gets premultiplied on the way in. Both end up in
127
+ // createSolidPicture, which hands them to XRender.
124
128
  function parseColor(value) {
125
129
  if (Array.isArray(value)) return value;
126
- const c = parseColorRaw(value);
127
- return [c.rgba[0] / 255, c.rgba[1] / 255, c.rgba[2] / 255, c.rgba[3]];
130
+ const c = cssColor(value);
131
+ if (!c) throw new Error(`Not a color: ${JSON.stringify(value)}`);
132
+ return c;
128
133
  }
129
134
 
130
135
  /**
@@ -4,16 +4,13 @@
4
4
  import { selectAll } from 'css-select';
5
5
  import postcss from 'postcss';
6
6
 
7
- import parseColorRaw from 'parse-color';
8
-
9
- /** parse a CSS color to premultipliable [r, g, b, a] floats, or null */
10
- export function cssColor(value) {
11
- if (!value) return null;
12
- if (value === 'transparent') return [0, 0, 0, 0];
13
- const c = parseColorRaw(String(value).trim());
14
- if (!c || !c.rgba) return null;
15
- return [c.rgba[0] / 255, c.rgba[1] / 255, c.rgba[2] / 255, c.rgba[3]];
16
- }
7
+ // Parse a CSS color to premultiplied [r, g, b, a] floats in 0..1, or null.
8
+ // These land on `ctx.fillStyle` as arrays (see htmlview's paint), which is
9
+ // the already-premultiplied form, so the premultiplying happens in color.js.
10
+ // Imported as well as re-exported: the cascade below calls it too.
11
+ import { cssColor } from '../color.js';
12
+
13
+ export { cssColor };
17
14
 
18
15
  const FONT_SIZE_KEYWORDS = {
19
16
  'xx-small': 9,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",
@@ -51,10 +51,14 @@
51
51
  "parse-color": "^1.0.0",
52
52
  "pngjs": "^7.0.0",
53
53
  "postcss": "^8.5.23",
54
- "x11": "^3.2.0",
54
+ "x11": "^3.3.0",
55
55
  "yoga-layout": "^3.2.1"
56
56
  },
57
57
  "scripts": {
58
- "test": "node --test"
58
+ "test": "NTK_STRICT_COLORS=1 node --test",
59
+ "check-release-message": "node scripts/check-release-message.mjs"
60
+ },
61
+ "devDependencies": {
62
+ "@conventional-commits/parser": "^0.4.1"
59
63
  }
60
64
  }