Skip to main content

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 doc directly

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. Use RGBA8() 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:

IntegerTypeDescription
0LinearStraight line from stop to stop
1EllipticalEllipse radiation from centre
2RadialCircular radiation from centre
3ConicalCone 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​

ChannelCommandWhat it controls
BrushcreateSetBrushFillInterior / body fill
PencreateSetPenFillStroke / outline fill
TransparencycreateSetTransparencyFillTransparency 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.Dash is required for dashes to appear. Without it the pattern is stored but Affinity renders solid. Always use LineStyleMask.All when 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().

ValueNameEffect
0CentreStroke sits evenly on both sides of the path (default)
1InsideStroke sits entirely inside the path boundary
2OutsideStroke 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:

EnumValues
DiffusionCurveKindBezier, Ellipse, Arc, Line
DiffusionCurveSideLeft, 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() has weight = 0 and the setter is silently ignored. Use LineStyle.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​

ExampleWhat it shows
meshBubble.jsColourMesh, MeshFill, FillDescriptor.create, createSetBrushFill — full mesh-gradient workflow
arrowheads.jsArrowHead, ArrowHeadStyle, LineStyleDescriptor.cloneWithNewArrowHeads
strokesWeightDown.jsDecrease stroke weight across a selection with LineStyle / LineStyleMask
strokesWeightUp.jsIncrease stroke weight across a selection with LineStyle / LineStyleMask

See also​


Example scripts​

05a-fill-and-stroke.js​

Download 05a-fill-and-stroke.js

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.');
}