Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 50 additions & 4 deletions docs/context-2d.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,20 @@ ctx.fillRect(0, 0, 100, 100);
- `ctx.shadowColor`, `ctx.shadowBlur`, `ctx.shadowOffsetX`,
`ctx.shadowOffsetY` — drop shadows, off by default (a transparent
`shadowColor`). See **Shadows** below
- `ctx.globalCompositeOperation` — Porter-Duff subset mapped to XRender ops:
`source-over` (default), `copy`, `destination-over`, `source-in`,
- `ctx.globalCompositeOperation` — mapped to XRender ops: the Porter-Duff
subset, `source-over` (default), `copy`, `destination-over`, `source-in`,
`destination-in`, `source-out`, `destination-out`, `source-atop`,
`destination-atop`, `xor`, `lighter`. Everything drawn takes it: fills,
`destination-atop`, `xor`, `lighter`, and the blend modes, `multiply`,
`screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`,
`hard-light`, `soft-light`, `difference`, `exclusion`, `hue`,
`saturation`, `color` and `luminosity`. Everything drawn takes it: fills,
strokes, rectangles, images, text (`fillText`, `TextLayout.draw`) and the
shadows they cast. The ops that write where a drawing has no ink clear the
rest of the drawing's own box, not the surface, and no op changes a pixel
outside the clip — see
[Composite ops that clear](#composite-ops-that-clear)
[Composite ops that clear](#composite-ops-that-clear). A name the context
cannot draw is ignored, as a browser ignores one it does not know, so read
it back to know — see [Blend modes](#blend-modes)
- `ctx.font` — the CSS `font` shorthand (`'bold italic 40px "DejaVu Sans"'`),
resolved through fontconfig; see [fonts.md](fonts.md). Style, weight,
`small-caps` and a stretch keyword come in any order before the size; a
Expand Down Expand Up @@ -119,6 +124,47 @@ The ops that write where a drawing has no ink — `copy`, `source-in`,
region the way every other op does, and a path clip after the op rather than
through the mask: see [Composite ops that clear](#composite-ops-that-clear).

### Blend modes

The blend modes are RENDER's from 0.11 (`Multiply` to `HSLLuminosity`),
which composite as CSS's `mix-blend-mode` does: what a pixel comes to is a
function of the colour drawn and the colour under it. A server that answered
`QueryVersion` below 0.11 has none of them, and raises `BadPictOp` for each
after the call has returned, so the context takes a blend mode's name only
where the server has them and leaves the op as it was otherwise. A caller
asks by setting one and reading it back:

```js
ctx.globalCompositeOperation = 'multiply';
if (ctx.globalCompositeOperation !== 'multiply') {
// this server cannot blend: draw it some other way
}
```

Every X.Org server since 2009 has them — Xorg, Xvfb, Xwayland, XQuartz — and
so does node-x11's in-process one. On Xvfb and XQuartz, and wherever the
server composites with fb, pixman draws them. glamor, under Xorg's
modesetting driver and Xwayland, has shaders for the Porter-Duff operators
alone, and falls back to software for these, reading what they are drawn
over back from the GPU: a large surface drawn with one costs more there than
one drawn `source-over`.

They take the clip, `globalAlpha` and a drawing's antialiased edge as
`source-over` does, through the mask: a transparent source leaves the
destination as it was through every one of them, and each is linear in the
source, so a pixel half covered is half of the way to the blend. pixman runs
`color-dodge`, `color-burn`, `soft-light` and the four HSL modes in single
floats and the rest in 8-bit integers; the in-process server matches the
second to the bit and the first within a level.

Older pixman draws the four HSL modes wrongly through a mask: Debian
bookworm's, 0.42.2, scales green by the mask twice and blue not at all,
where 0.46.4 is right. So on a server built on it, `hue`, `saturation`,
`color` and `luminosity` come out off in green and blue wherever there is a
mask: under `globalAlpha` below 1, a path clip, and the antialiased edge of
anything but a whole-pixel rectangle or image. Nothing on the client can
tell or correct it.

### Composite ops that clear

`copy`, `source-in`, `destination-in`, `source-out` and `destination-atop`
Expand Down
54 changes: 52 additions & 2 deletions lib/renderingcontext_2d.js
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,30 @@ const GCO_TO_PICTOP = {
lighter: "Add",
};

// The blend modes, which RENDER has from 0.11 (Multiply .. HSLLuminosity):
// what a pixel comes to is a function of both colours, as CSS's
// mix-blend-mode has it. A server that answers QueryVersion below 0.11 raises
// BadPictOp for every one of them, after the call has returned, so the setter
// takes one of these names only where the server has them (`_blendModes`).
// Reading the name back is how a caller asks whether it will be drawn.
const BLEND_TO_PICTOP = {
multiply: "Multiply",
screen: "Screen",
overlay: "Overlay",
darken: "Darken",
lighten: "Lighten",
"color-dodge": "ColorDodge",
"color-burn": "ColorBurn",
"hard-light": "HardLight",
"soft-light": "SoftLight",
difference: "Difference",
exclusion: "Exclusion",
hue: "HSLHue",
saturation: "HSLSaturation",
color: "HSLColor",
luminosity: "HSLLuminosity",
};

// The ops whose result is the destination wherever the mask is zero. Only
// those may have a drawing's mask split into several boxes (maskcluster.js):
// what the split gives up is the gaps between the boxes, and for these ops
Expand All @@ -663,13 +687,19 @@ const GCO_TO_PICTOP = {
// the source — or nothing — across the whole box, so they keep one. Nor can
// a path clip be folded into their mask: it is applied after them instead
// (`_clipAfter`).
//
// Every blend mode is one of them: a transparent source leaves the
// destination as it was. And a clip folded into the mask comes to what it
// would after the op, since each is linear in the source — through a mask of
// a half, `multiply` is half of the way to `multiply`.
const MASK_BOUNDED_OPS = [
"Over",
"OverReverse",
"OutReverse",
"Atop",
"Xor",
"Add",
...Object.values(BLEND_TO_PICTOP),
];

// and what they are clustered with instead: one mask, exactly as before
Expand Down Expand Up @@ -1297,11 +1327,31 @@ class RenderingContext2d {
}

_op() {
return this.Render.PictOp[GCO_TO_PICTOP[this._gco] || "Over"];
const name = Object.hasOwn(GCO_TO_PICTOP, this._gco)
? GCO_TO_PICTOP[this._gco]
: BLEND_TO_PICTOP[this._gco];
return this.Render.PictOp[name];
}

/**
* Whether the server composites RENDER 0.11's blend operators: whether it
* answered QueryVersion with 0.11 or later. Every X.Org server since 2009
* does — Xorg, Xvfb, Xwayland, XQuartz — and so does node-x11's
* in-process one, which composites them from the release this needs.
*/
_blendModes() {
const [major, minor] = this.Render.version || [0, 0];
return major > 0 || minor >= 11;
}

// An operator canvas names and this context cannot draw is ignored, as a
// browser ignores one it does not know: the name a caller reads back is
// the one that is drawn.
set globalCompositeOperation(value) {
if (value in GCO_TO_PICTOP) this._gco = value;
if (Object.hasOwn(GCO_TO_PICTOP, value)) this._gco = value;
else if (Object.hasOwn(BLEND_TO_PICTOP, value) && this._blendModes()) {
this._gco = value;
}
}

get globalCompositeOperation() {
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0",
"x11": "^4.1.0"
"x11": "^4.4.0"
},
"optionalDependencies": {
"x11-dri": ">=0.5.0 <1"
Expand Down
Loading
Loading