Lesson 05 — Fills & Strokes
Fills and strokes are the visual language of vector design. This lesson covers how the SDK represents colour, how to construct any fill type — solid, gradient, or none — and how to apply stroke styles including weight, caps, joins, and dash patterns to selected nodes.
Almost every script that does something visible will need to set a fill or a stroke. The SDK's approach is more structured than you might expect: fills are wrapped in a FillDescriptor, strokes are described by a LineStyle, and partial updates require a LineStyleMask. Understanding this structure means you can apply exactly the visual properties you want without accidentally overwriting ones you meant to keep.
What this lesson covers
- Creating colours with
RGBA8() FillDescriptor— the wrapper for any fill type- Solid, gradient, hatch, and bitmap fills
- Line styles — weight, cap, join, dash patterns
LineStyleMask— partial style updates- Brush fill (interior) vs pen fill (stroke) vs transparency fill
- Setting opacity and blend mode
- Convenience fill methods on
docdirectly
Colours
Create colours with the RGBA8(r, g, b, a) convenience function — values are 0–255 integers.
'use strict';
const { RGBA8, Colour } = require('/colours.js');
// RGBA8(red, green, blue, alpha) — values 0–255
const red = RGBA8(220, 50, 50, 255);
const blue = RGBA8(40, 120, 220, 255);
const semiBlack = RGBA8(0, 0, 0, 128); // 50% opacity
const clear = RGBA8(0, 0, 0, 0); // fully transparent
// Alternative — same result
const red2 = RGBA8(220, 50, 50, 255);
Never use
Colour.fromRGBA()— it does not exist in the SDK. UseRGBA8()instead.
FillDescriptor
Wrap a fill type in FillDescriptor so it can be passed to command factories like createSetBrushFill.
'use strict';
const { FillDescriptor, SolidFill } = require('/fills.js');
const { RGBA8 } = require('/colours.js');
// Solid colour fill
const colour = RGBA8(200, 80, 40, 255);
const fill = FillDescriptor.createSolid(colour);
// No fill (transparent)
const none = FillDescriptor.createNone();
makeFillDescriptor() coerces for you: colours and solid fills become descriptors, null becomes a no-fill descriptor, existing descriptors pass through unchanged. Fill.fromFill(fill) (and the per-class SolidFill.fromFill, GradientFill.fromFill, DiffusionFill.fromFill, MeshFill.fromFill, BitmapFill.fromFill) downcasts a generic Fill to the concrete subclass. Immutable variants clone with one field changed: cloneWithNewTransform, cloneWithNewTransformInfo, cloneWithNewBlendMode, cloneWithNewFill, cloneWithNewCurves, cloneWithNewIsAnchoredToSpread, cloneWithNewIsScaleWithObject (plus the cloneWithNewGradient* pair used above); getTransformInfo() reads the transform back.
Gradient fills
Create a GradientFill from its default black→white state, then switch gradient shape with cloneWithNewGradientFillType(type). Affinity supports four gradient types:
| Integer | Type | Description |
|---|---|---|
0 | Linear | Straight line from stop to stop |
1 | Elliptical | Ellipse radiation from centre |
2 | Radial | Circular radiation from centre |
3 | Conical | Cone sweeping around a point |
Prefer the GradientFillType enum over raw integers where available. Values runtime-verified: Linear=0, Elliptical=1, Radial=2, Conical=3 (see SDK-EXPOSURE-MAP.md enum reference). Custom stop colours need a pre-built Gradient from /colours.js:
// Snippet — assumes RGBA8 is imported
const { Gradient } = require('/colours.js');
const { GradientFill } = require('/fills.js');
const gradient = Gradient.create([
{ colour: RGBA8(255, 0, 0, 255), position: 0 },
{ colour: RGBA8(0, 0, 255, 255), position: 1 },
]);
const gradFill = GradientFill.create(gradient, 0); // 0 = linear
'use strict';
const { GradientFill, FillDescriptor } = require('/fills.js');
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select something first.'); }
else {
const sel = doc.selection;
// Start from the default gradient (black → white, linear)
const baseGf = GradientFill.createDefault();
// Apply a linear gradient (type 0)
const linearFd = FillDescriptor.create(baseGf, true);
doc.executeCommand(DocumentCommand.createSetBrushFill(sel, linearFd));
console.log('Linear gradient applied.');
// Switch to radial (type 2) — same gradient, different shape
const radialGf = baseGf.cloneWithNewGradientFillType(2);
const radialFd = FillDescriptor.create(radialGf, true);
doc.executeCommand(DocumentCommand.createSetBrushFill(sel, radialFd));
console.log('Radial gradient applied.');
// Conical (type 3)
const conicalGf = baseGf.cloneWithNewGradientFillType(3);
const conicalFd = FillDescriptor.create(conicalGf, true);
doc.executeCommand(DocumentCommand.createSetBrushFill(sel, conicalFd));
console.log('Conical gradient applied.');
}
Reading gradient stop data
Inspect the gradient stops — their positions, midpoints, and colours — via GradientFill.gradient.
'use strict';
const { GradientFill } = require('/fills.js');
const gf = GradientFill.createDefault();
const grad = gf.gradient;
console.log('Stop count:', grad.stopCount);
for (let i = 0; i < grad.stopCount; i++) {
const stop = grad.getStop(i);
console.log(`Stop ${i}: position=${stop.position} midpoint=${stop.midpoint}`);
}
The SDK exposes GradientFill.createDefault() for a basic black-to-white gradient, and cloneWithNewGradientFillType(type) to switch the shape. Fine-grained stop colour modification via Gradient.getStop(i).colour is readable but setting stop colours programmatically requires passing a pre-built Gradient object to GradientFill.create(gradient, type), which in turn requires the colours.js Gradient class stop-building API.
Gradient colours are controlled interactively in the Affinity UI gradient editor.
Applying fills to selected nodes
Use createSetBrushFill for interior fill and createSetPenFill for stroke fill — both take the selection first, then the fill.
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { FillDescriptor } = require('/fills.js');
const { RGBA8 } = require('/colours.js');
const doc = Document.current;
if (!doc || doc.selection.nodes.isEmpty) {
console.log('Select something first.');
} else {
const sel = doc.selection;
const colour = RGBA8(255, 140, 0, 255); // orange
const fill = FillDescriptor.createSolid(colour);
// Brush fill = interior fill (options object first)
doc.executeCommand(DocumentCommand.createSetBrushFill(sel, fill));
// Pen fill = stroke fill (use a different colour)
const strokeColour = RGBA8(40, 40, 40, 255);
const strokeFill = FillDescriptor.createSolid(strokeColour);
doc.executeCommand(DocumentCommand.createSetPenFill(sel, strokeFill));
}
Three fill channels
| Channel | Command | What it controls |
|---|---|---|
| Brush | createSetBrushFill | Interior / body fill |
| Pen | createSetPenFill | Stroke / outline fill |
| Transparency | createSetTransparencyFill | Transparency mask fill |
Line styles
To change a line's weight, clone the node's current line style via lineStyleInterface rather than using LineStyle.createDefault() — see the weight note below.
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { LineStyleMask } = require('/linestyle.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select something first.'); }
else {
const sel = doc.selection;
const node = sel.nodes.first;
// Clone the node's current line style — do NOT use LineStyle.createDefault()
// because its weight is 0 and the setter is silently ignored.
// Use LineStyle.createDefaultWithWeight(n) instead if you need a fresh style.
const currentStyle = node.lineStyleInterface?.lineStyleDescriptor?.lineStyle;
if (currentStyle) {
const style = currentStyle.clone();
style.weight = 3 * (doc.dpi / 72); // 3pt in document pixels
doc.executeCommand(DocumentCommand.createSetLineStyle(sel, style,
{ lineStyleMask: LineStyleMask.Weight }));
console.log('Line weight set to 3pt.');
}
}
createDefault() returns a style with weight = 0, and the .weight setter is silently ignored — setting it has no effect. Use LineStyle.createDefaultWithWeight(n) if you need a fresh style with a specific weight, or clone the existing style from node.lineStyleInterface.lineStyleDescriptor.lineStyle to modify the current weight.
For weight, clone the node's current line style or use
LineStyle.createDefaultWithWeight(n)—LineStyle.createDefault()can't set a usable weight. For cap/join/dash properties (which aren't affected),createDefault()is fine.
Converting points to pixels
Affinity works in 96dpi pixels internally. If you want a weight in points (like in the UI):
// Snippet — assumes doc is already defined (e.g. const doc = Document.current)
const weightInPts = 2;
const weightInPx = weightInPts * (doc.dpi / 72);
style.weight = weightInPx;
Or use the doc shorthand which handles the pts-to-pixels conversion automatically:
'use strict';
const { Document } = require('/document.js');
const { LineStyleMask } = require('/linestyle.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select something first.'); }
else {
// doc.setLineWeightPts converts points to document pixels automatically
doc.setLineWeightPts(2, doc.selection);
console.log('Weight set to 2pt.');
}
Line caps and joins
Set .cap and .join to the desired integer value and apply with LineStyleMask.Cap / LineStyleMask.Join. Because cap/join are independent of weight, LineStyle.createDefault() is fine here — the weight caveat above doesn't apply.
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand, CompoundCommandBuilder } = require('/commands.js');
const { LineStyle, LineStyleMask } = require('/linestyle.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select something first.'); }
else {
const sel = doc.selection;
const compound = CompoundCommandBuilder.create();
// Set round caps
const capStyle = LineStyle.createDefault();
// Cap values: 0 = Butt, 1 = Square, 2 = Round
capStyle.cap = 2;
compound.addCommand(DocumentCommand.createSetLineStyle(sel, capStyle,
{ lineStyleMask: LineStyleMask.Cap }));
// Set round joins
const joinStyle = LineStyle.createDefault();
// Join values: 0/1 = Miter, 2 = Round, 3 = Bevel
joinStyle.join = 2;
compound.addCommand(DocumentCommand.createSetLineStyle(sel, joinStyle,
{ lineStyleMask: LineStyleMask.Join }));
doc.executeCommand(compound.createCommand());
console.log('Round caps and joins applied.');
}
Dash patterns
For dashes to be visible you must set three things: the line type to Dash, a non-zero weight, and the dash pattern. Setting only DashPattern stores the values but Affinity continues to render a solid line. Apply all three in one command using LineStyleMask.All.
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand, CompoundCommandBuilder } = require('/commands.js');
const { LineStyleMask, LineType } = require('/linestyle.js');
const { FillDescriptor } = require('/fills.js');
const { RGBA8 } = require('/colours.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select something first.'); }
else {
const sel = doc.selection;
const compound = CompoundCommandBuilder.create();
// Set a visible stroke colour (selection first)
compound.addCommand(DocumentCommand.createSetPenFill(
sel, FillDescriptor.createSolid(RGBA8(30, 30, 30, 255))));
// Clone current style — set weight, type AND dash together, apply with All mask
const node = sel.nodes.first;
const currentStyle = node.lineStyleInterface?.lineStyleDescriptor?.lineStyle;
if (currentStyle) {
const dashStyle = currentStyle.clone();
dashStyle.weight = 2 * (doc.dpi / 72); // 2pt in document pixels
dashStyle.type = LineType.Dash; // REQUIRED — renders as solid without this
dashStyle.dashPattern = [8, 4]; // 8px dash, 4px gap
dashStyle.hasBalancedDashes = true;
compound.addCommand(DocumentCommand.createSetLineStyle(sel, dashStyle,
{ lineStyleMask: LineStyleMask.All }));
}
doc.executeCommand(compound.createCommand());
console.log('Dashed stroke applied.');
}
LineType.Dashis required for dashes to appear. Without it the pattern is stored but Affinity renders solid. Always useLineStyleMask.Allwhen setting type, weight, and pattern together.
LineStyleMask — partial updates
Apply only specific parts of a line style, leaving the rest unchanged:
// Snippet — assumes doc, sel and style are already defined
// Only change the weight — cap, join, dash stay as they are
doc.setLineStyle(style, sel, { lineStyleMask: LineStyleMask.Weight });
// Available masks:
// LineStyleMask.Weight, .Cap, .Join, .DashPattern, .DashPhase,
// .BalancedDashes, .Type, .All
Stroke alignment
Control whether the stroke sits inside, outside, or centred on the path using doc.setStrokeAlignment().
| Value | Name | Effect |
|---|---|---|
0 | Centre | Stroke sits evenly on both sides of the path (default) |
1 | Inside | Stroke sits entirely inside the path boundary |
2 | Outside | Stroke sits entirely outside the path boundary |
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { StrokeAlignment, LineStyleMask } = require('/linestyle.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select something first.'); }
else {
const sel = doc.selection;
const style = require('/linestyle.js').LineStyle.createDefault();
// StrokeAlignment is in /linestyle — not in affinity:dom
doc.setStrokeAlignment(StrokeAlignment.Centre, sel);
console.log('Stroke alignment set to Centre.');
}
Opacity and blend mode
Set overall layer opacity or fill-specific opacity, and apply blend modes like Multiply or Screen.
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const doc = Document.current;
const sel = doc.selection;
// Layer opacity (0.0–1.0)
doc.executeCommand(DocumentCommand.createSetOpacity(sel, 0.5));
// Or use the doc convenience method:
doc.setOpacity(0.5, sel);
// Blend mode — BlendMode enum from affinity:blendmode
// doc.setBlendMode(blendMode, setPassthrough, selection)
// common values: Normal, Multiply, Screen, Overlay, SoftLight, HardLight, Darken, Lighten
// Fill-level opacity (separate from layer opacity)
doc.setBrushFillOpacity(0.7, sel); // interior fill opacity
doc.setLineFillOpacity(0.5, sel); // stroke fill opacity
Hatch fills
Hatch fills tile a repeating line pattern over the shape. Start from HatchPattern.createDefault(), then build a HatchFill and wrap it in a FillDescriptor.
'use strict';
const { HatchFill, FillDescriptor } = require('/fills.js');
const { HatchPattern } = require('/hatch.js');
const { Colour } = require('/colours.js');
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const doc = Document.current;
const sel = doc.selection;
// Start from the default hatch pattern (customise lines via appendHatchLine)
const pattern = HatchPattern.createDefault();
const penColor = new Colour(/* ... */);
const hatch = HatchFill.create(
pattern, // HatchPattern
1, // units (1 = document units)
penColor, // line colour
null, // background colour (null = transparent)
1.0 // line weight
);
const fill = FillDescriptor.create(hatch, false, null, null, false);
doc.executeCommand(DocumentCommand.createSetBrushFill(sel, fill));
Bitmap fills
Load a raster image from disk with Bitmap.loadFromFile, then wrap it in a BitmapFill and apply as a fill pattern.
'use strict';
const { BitmapFill, FillDescriptor } = require('/fills.js');
const { Bitmap } = require('/rasterobject.js');
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { app } = require('/application.js');
const doc = Document.current;
const bitmap = Bitmap.loadFromFile(app.userDesktopPath + '/texture.png');
if (bitmap) {
const bitmapFill = BitmapFill.create(bitmap, 0, 0, false);
const fill = FillDescriptor.create(bitmapFill, true, null, null, false);
doc.executeCommand(DocumentCommand.createSetBrushFill(doc.selection, fill));
console.log('Bitmap fill applied.');
}
Mesh fills
A mesh fill places a grid of colour nodes over a shape and interpolates smoothly between them — the same technology as the Mesh Gradient tool in the UI. Build a ColourMesh with createFromColour(colour, nodes), set each node's position and colour, then wrap in a MeshFill. ColourMesh.createFromGradient(gradient, isRadial) builds the grid from a gradient instead (linear or radial), and createDefaultNone() / createDefaultWhite() give empty and white presets.
'use strict';
const { ColourMesh, MeshFill, FillDescriptor } = require('/fills.js');
const { RGBA8 } = require('/colours.js');
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const doc = Document.current;
if (!doc || doc.selection.nodes.isEmpty) { console.log('Select a shape.'); }
else {
const mesh = ColourMesh.createFromColour(RGBA8(255, 255, 255, 255), 4);
mesh.setNodeColour(0, 0, RGBA8(255, 0, 0, 255)); // top-left: red
mesh.setNodeColour(4, 0, RGBA8(0, 0, 255, 255)); // top-right: blue
mesh.setNodeColour(0, 4, RGBA8(0, 255, 0, 255)); // bottom-left: green
mesh.setNodeColour(4, 4, RGBA8(255, 255, 0, 255)); // bottom-right: yellow
const fill = FillDescriptor.create(MeshFill.create(mesh), true);
doc.executeCommand(DocumentCommand.createSetBrushFill(doc.selection, fill));
console.log('Mesh fill applied.');
}
ColourMesh exposes setNodePosition(x, y, {x, y}), setNodeColour(x, y, colour), setCurveNodePosition(x, y, MeshDirection.X, {x, y}), and a .size property.
Diffusion fills
Diffusion curves use overlapping geometric curves to define smooth colour regions (like the UI's diffusion-curve fill). Build a DiffusionCurveSet, add parametric curves, then wrap in a DiffusionFill.
'use strict';
const { DiffusionFill, DiffusionCurveSet, DiffusionCurveParametric, FillDescriptor } = require('/fills.js');
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const doc = Document.current;
if (!doc || doc.selection.nodes.isEmpty) { console.log('Select a shape.'); }
else {
const curveSet = DiffusionCurveSet.create();
// Add DiffusionCurveParametric entries to the set, then:
const fill = FillDescriptor.create(DiffusionFill.create(curveSet), true);
doc.executeCommand(DocumentCommand.createSetBrushFill(doc.selection, fill));
console.log('Diffusion fill applied.');
}
Supporting enums:
| Enum | Values |
|---|---|
DiffusionCurveKind | Bezier, Ellipse, Arc, Line |
DiffusionCurveSide | Left, Right |
Edit curves individually by index: getCurvePoints / setCurvePoints, getCurveColour / setCurveColour (per side), getCurveBlur / setCurveBlur, getCurveStrength / setCurveStrength, getCurvePressure / setCurvePressure, plus getCurveKind, getCurveParametric, getNodeColour, and removeCurve(index).
Doc convenience fill methods
Document exposes shorthand methods that accept a Colour, SolidFill, or FillDescriptor directly — no need to call DocumentCommand.createSetBrushFill explicitly:
// Snippet
const doc = Document.current;
const sel = doc.selection;
// All three accept Colour, SolidFill, or FillDescriptor
doc.setBrushFillDescriptor(myColour, sel); // interior fill
doc.setPenFillDescriptor(myFillDescriptor, sel); // stroke fill
doc.setTransparencyFillDescriptor(myFill, sel); // transparency
// Line style convenience — no LineStyle object needed
doc.setLineWeight(4, sel); // weight in pixels
doc.setLineWeightPts(2, sel); // weight in points (auto-converts to pixels)
doc.setLineCap(LineCap.Round, sel);
doc.setLineJoin(LineJoin.Round, sel);
doc.setStrokeAlignment(StrokeAlignment.Inside, sel);
// Dash enhancements
doc.setDashPhase(0, sel); // dash phase offset
doc.setBalancedDashes(true, sel); // balanced dashes at corners
VectorNodeDefinition — multi-fill and multi-stroke
When creating vector nodes programmatically, VectorNodeDefinition supports stacking multiple fill descriptors and line descriptor pairs. This is how Affinity represents nodes that have more than one fill (e.g. a shape with both a solid fill and a gradient fill layered).
'use strict';
const { Document } = require('/document.js');
const { PolyCurveNodeDefinition, VectorNodeDefinition } = require('/nodes.js');
const { AddChildNodesCommandBuilder } = require('/commands.js');
const { FillDescriptor, SolidFill, GradientFill, NoFill } = require('/fills.js');
const { LineStyle, LineStyleDescriptor } = require('/linestyle.js');
const { RGBA8 } = require('/colours.js');
const doc = Document.current;
if (doc) {
const def = PolyCurveNodeDefinition.createDefault();
// --- Multiple brush fills ---
const solid = FillDescriptor.createSolid(RGBA8(200, 80, 40, 255));
const gradient = FillDescriptor.create(GradientFill.createDefault(), true);
def.addBrushFillDescriptor(solid); // fill slot 0
def.addBrushFillDescriptor(gradient); // fill slot 1
console.log('Fill count:', def.brushFillDescriptorCount); // 2
def.currentBrushFillIndex = 0; // which fill is active when rendering
// Replace or remove a fill slot
// def.setBrushFillDescriptor(newFillDescriptor, 0); // fill, then index
// def.removeBrushFillDescriptor(1);
// --- Multiple line (stroke) descriptor pairs ---
const ls = LineStyle.createDefault();
ls.weight = 2;
const lsd = LineStyleDescriptor.createDefault(ls);
const lineFill = FillDescriptor.createSolid(RGBA8(40, 40, 40, 255));
def.addLineDescriptors(lineFill, lsd); // stroke slot 0
console.log('Stroke count:', def.lineDescriptorsCount); // 1
}
For most scripts you will only use one fill and one stroke — addBrushFillDescriptor and addLineDescriptors with a single call each. The multi-fill API is primarily useful when reproducing a node's visual state exactly from an existing document.
Fluent node-definition builders
Node-definition property setters return the definition, enabling fluent chains:
// Snippet
const def = PolyCurveNodeDefinition.createDefault()
.setUserDescription('My Shape')
.setTransform(someTransform);
const adjDef = BrightnessContrastAdjustmentRasterNodeDefinition.create(params)
.setParameters(params)
.setUserDescription('Brightness fix')
.setTransform(xf);
Colour accepted where FillDescriptor expected
VectorNodeDefinition fill/line methods accept a Colour directly — a solid fill is created automatically:
// Snippet
const def = PolyCurveNodeDefinition.createDefault();
const { RGBA8 } = require('/colours.js');
def.addBrushFillDescriptor(RGBA8(200, 80, 40, 255));
def.setBrushFillDescriptor(0, RGBA8(100, 180, 60, 255));
LineType.Dash is required for dashes to render. Setting dashPattern alone stores the values but Affinity continues to render a solid line. Always set style.type = LineType.Dash and apply with LineStyleMask.All.
createSetBrushFill and createSetPenFill take selection first. The signature is createSetBrushFill(selection, fill, options) — selection first, then the fill (a FillDescriptor, SolidFill, or Colour), then an optional options object. Use createSetBrushFill(sel, myFill).
copyTransform option in createSetBrushFill — when true (default), gradient fill transforms are copied from the selection's bounding box. Set to false if you want the gradient anchored independently.
LineStyle.createDefault()hasweight = 0and the setter is silently ignored. UseLineStyle.createDefaultWithWeight(n)for a fresh style with a specific weight.
Exercises
Exercise 5.1 — Solid fill batch
Select several shapes. Apply a different solid colour from a predefined palette to each one.
Exercise 5.2 — Stroke styler
Write a script that gives every selected node a 2pt round-capped dashed stroke, without changing their fill colours. Use LineStyleMask to target only the relevant properties.
Exercise 5.3 — Opacity fade
Apply decreasing opacity to each node in the selection (100%, 80%, 60%, 40%, 20%) using setTransparencyFillDescriptor.
SDK modules used in this lesson
/colours—Colour,RGBA8/fills—FillDescriptor,SolidFill,GradientFill,HatchFill,BitmapFill,MeshFill,DiffusionFill,NoFill/hatch—HatchPattern(for hatch fills)/linestyle—LineStyle,LineStyleMask,LineType,StrokeAlignment/commands—DocumentCommand,CompoundCommandBuilder/document—Document
Further examples
| Example | What it shows |
|---|---|
meshBubble.js | ColourMesh, MeshFill, FillDescriptor.create, createSetBrushFill — full mesh-gradient workflow |
arrowheads.js | ArrowHead, ArrowHeadStyle, LineStyleDescriptor.cloneWithNewArrowHeads |
strokesWeightDown.js | Decrease stroke weight across a selection with LineStyle / LineStyleMask |
strokesWeightUp.js | Increase stroke weight across a selection with LineStyle / LineStyleMask |
See also
Example scripts
05a-fill-and-stroke.js
Download 05a-fill-and-stroke.js
'use strict';
// Example 05a — Fills and strokes
// Lesson 05: Fills & Strokes
//
// Applies a solid fill and a visible dashed stroke to all selected nodes.
//
// KEY INSIGHTS:
// 1. Clone the node's CURRENT line style — LineStyle.createDefault() has weight=0
// and the weight setter is silently ignored.
// 2. Set style.type = LineType.Dash — without this the dash pattern is stored
// but Affinity renders a solid line.
// 3. Apply weight + type + dash in one command using LineStyleMask.All.
const { Document } = require('/document.js');
const { DocumentCommand, CompoundCommandBuilder } = require('/commands.js');
const { Selection } = require('/selections.js');
const { FillDescriptor } = require('/fills.js');
const { LineStyle, LineStyleMask, LineType } = require('/linestyle.js');
const { RGBA8 } = require('/colours.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) {
console.log('Select some objects and run again.');
} else {
const compound = CompoundCommandBuilder.create();
for (const node of doc.selection.nodes) {
const nodeSel = Selection.create(doc, node);
// 1. Solid orange fill (selection first)
compound.addCommand(DocumentCommand.createSetBrushFill(
nodeSel, FillDescriptor.createSolid(RGBA8(255, 140, 0, 255))));
// 2. Dark stroke colour (selection first)
compound.addCommand(DocumentCommand.createSetPenFill(
nodeSel, FillDescriptor.createSolid(RGBA8(30, 30, 30, 255))));
// 3. Stroke: clone current style, set weight + type + dash, apply all at once
const currentStyle = node.lineStyleInterface?.lineStyleDescriptor?.lineStyle;
if (currentStyle) {
const strokeStyle = currentStyle.clone();
strokeStyle.weight = 2 * (doc.dpi / 72); // 2pt in document pixels
strokeStyle.type = LineType.Dash; // REQUIRED for dashes to show
strokeStyle.dashPattern = [6, 3]; // 6px dash, 3px gap
strokeStyle.hasBalancedDashes = true;
compound.addCommand(DocumentCommand.createSetLineStyle(nodeSel, strokeStyle,
{ lineStyleMask: LineStyleMask.All }));
}
}
doc.executeCommand(compound.createCommand());
console.log('Fill and dashed stroke applied.');
}