Skip to content

Commit ccde850

Browse files
committed
Add strokeAttenuation property for constant stroke width
Introduces the strokeAttenuation property to Path, Points, Text, and Group objects, allowing stroke width to remain constant in screen space regardless of transformations when set to false. Adds getEffectiveStrokeWidth utility and updates renderers to use it for stroke width calculations. This enables more flexible visual control over stroke rendering, especially for zooming and scaling scenarios.
1 parent 037675e commit ccde850

4 files changed

Lines changed: 283 additions & 37 deletions

File tree

build/two.js

Lines changed: 140 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ var Two = (() => {
116116
TWO_PI: () => TWO_PI,
117117
decomposeMatrix: () => decomposeMatrix,
118118
getComputedMatrix: () => getComputedMatrix,
119+
getEffectiveStrokeWidth: () => getEffectiveStrokeWidth,
119120
getPoT: () => getPoT,
120121
lerp: () => lerp,
121122
mod: () => mod,
@@ -208,6 +209,25 @@ var Two = (() => {
208209
function toFixed(v) {
209210
return floor(v * 1e6) / 1e6;
210211
}
212+
function getEffectiveStrokeWidth(object, worldMatrix) {
213+
const linewidth = object._linewidth;
214+
if (object.strokeAttenuation) {
215+
return linewidth;
216+
}
217+
if (!worldMatrix) {
218+
worldMatrix = object.worldMatrix || getComputedMatrix(object);
219+
}
220+
const decomposed = decomposeMatrix(
221+
worldMatrix.elements[0],
222+
worldMatrix.elements[3],
223+
worldMatrix.elements[1],
224+
worldMatrix.elements[4],
225+
worldMatrix.elements[2],
226+
worldMatrix.elements[5]
227+
);
228+
const scale = Math.max(Math.abs(decomposed.scaleX), Math.abs(decomposed.scaleY));
229+
return scale > 0 ? linewidth / scale : linewidth;
230+
}
211231

212232
// src/utils/path-commands.js
213233
var Commands = {
@@ -1200,7 +1220,7 @@ var Two = (() => {
12001220
* @name Two.PublishDate
12011221
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
12021222
*/
1203-
PublishDate: "2025-09-30T22:51:48.793Z",
1223+
PublishDate: "2025-09-30T22:57:13.725Z",
12041224
/**
12051225
* @name Two.Identifier
12061226
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
@@ -4272,6 +4292,12 @@ var Two = (() => {
42724292
* @property {Boolean} - Determines whether the {@link Two.Path#miter} needs updating.
42734293
*/
42744294
_flagMiter = true;
4295+
/**
4296+
* @name Two.Path#_flagStrokeAttenuation
4297+
* @private
4298+
* @property {Boolean} - Determines whether the {@link Two.Path#strokeAttenuation} needs updating.
4299+
*/
4300+
_flagStrokeAttenuation = true;
42754301
/**
42764302
* @name Two.Path#_flagMask
42774303
* @private
@@ -4387,6 +4413,12 @@ var Two = (() => {
43874413
* @see {@link Two.Path#dashes}
43884414
*/
43894415
_dashes = null;
4416+
/**
4417+
* @name Two.Path#_strokeAttenuation
4418+
* @private
4419+
* @see {@link Two.Path#strokeAttenuation}
4420+
*/
4421+
_strokeAttenuation = true;
43904422
constructor(vertices, closed2, curved, manual) {
43914423
super();
43924424
for (let prop in proto10) {
@@ -4436,7 +4468,8 @@ var Two = (() => {
44364468
"automatic",
44374469
"beginning",
44384470
"ending",
4439-
"dashes"
4471+
"dashes",
4472+
"strokeAttenuation"
44404473
];
44414474
static Utils = {
44424475
getCurveLength: getCurveLength2
@@ -5062,7 +5095,7 @@ var Two = (() => {
50625095
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
50635096
*/
50645097
flagReset() {
5065-
this._flagVertices = this._flagLength = this._flagFill = this._flagStroke = this._flagLinewidth = this._flagOpacity = this._flagVisible = this._flagCap = this._flagJoin = this._flagMiter = this._flagClip = false;
5098+
this._flagVertices = this._flagLength = this._flagFill = this._flagStroke = this._flagLinewidth = this._flagOpacity = this._flagVisible = this._flagCap = this._flagJoin = this._flagMiter = this._flagClip = this._flagStrokeAttenuation = false;
50665099
Shape.prototype.flagReset.call(this);
50675100
return this;
50685101
}
@@ -5291,6 +5324,22 @@ var Two = (() => {
52915324
}
52925325
this._dashes = v;
52935326
}
5327+
},
5328+
/**
5329+
* @name Two.Path#strokeAttenuation
5330+
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
5331+
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
5332+
*/
5333+
strokeAttenuation: {
5334+
enumerable: true,
5335+
get: function() {
5336+
return this._strokeAttenuation;
5337+
},
5338+
set: function(v) {
5339+
this._strokeAttenuation = !!v;
5340+
this._flagStrokeAttenuation = true;
5341+
this._flagLinewidth = true;
5342+
}
52945343
}
52955344
};
52965345
function FlagVertices() {
@@ -6769,6 +6818,7 @@ var Two = (() => {
67696818
_flagVisible = true;
67706819
_flagSize = true;
67716820
_flagSizeAttenuation = true;
6821+
_flagStrokeAttenuation = true;
67726822
_length = 0;
67736823
_fill = "#fff";
67746824
_stroke = "#000";
@@ -6780,6 +6830,7 @@ var Two = (() => {
67806830
_beginning = 0;
67816831
_ending = 1;
67826832
_dashes = null;
6833+
_strokeAttenuation = true;
67836834
constructor(vertices) {
67846835
super();
67856836
for (let prop in proto16) {
@@ -6821,7 +6872,8 @@ var Two = (() => {
68216872
"sizeAttenuation",
68226873
"beginning",
68236874
"ending",
6824-
"dashes"
6875+
"dashes",
6876+
"strokeAttenuation"
68256877
];
68266878
/**
68276879
* @name Two.Points.fromObject
@@ -7216,6 +7268,22 @@ var Two = (() => {
72167268
}
72177269
this._dashes = v;
72187270
}
7271+
},
7272+
/**
7273+
* @name Two.Points#strokeAttenuation
7274+
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
7275+
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
7276+
*/
7277+
strokeAttenuation: {
7278+
enumerable: true,
7279+
get: function() {
7280+
return this._strokeAttenuation;
7281+
},
7282+
set: function(v) {
7283+
this._strokeAttenuation = !!v;
7284+
this._flagStrokeAttenuation = true;
7285+
this._flagLinewidth = true;
7286+
}
72197287
}
72207288
};
72217289

@@ -8071,9 +8139,9 @@ var Two = (() => {
80718139
*/
80728140
_flagVisible = true;
80738141
/**
8074-
* @name Two.Path#_flagMask
8142+
* @name Two.Text#_flagMask
80758143
* @private
8076-
* @property {Boolean} - Determines whether the {@link Two.Path#mask} needs updating.
8144+
* @property {Boolean} - Determines whether the {@link Two.Text#mask} needs updating.
80778145
*/
80788146
_flagMask = false;
80798147
/**
@@ -8088,6 +8156,12 @@ var Two = (() => {
80888156
* @property {Boolean} - Determines whether the {@link Two.Text#direction} needs updating.
80898157
*/
80908158
_flagDirection = true;
8159+
/**
8160+
* @name Two.Text#_flagStrokeAttenuation
8161+
* @private
8162+
* @property {Boolean} - Determines whether the {@link Two.Text#strokeAttenuation} needs updating.
8163+
*/
8164+
_flagStrokeAttenuation = true;
80918165
// Underlying Properties
80928166
/**
80938167
* @name Two.Text#value
@@ -8187,6 +8261,12 @@ var Two = (() => {
81878261
* @see {@link Two.Text#dashes}
81888262
*/
81898263
_dashes = null;
8264+
/**
8265+
* @name Two.Text#_strokeAttenuation
8266+
* @private
8267+
* @see {@link Two.Text#strokeAttenuation}
8268+
*/
8269+
_strokeAttenuation = true;
81908270
constructor(message, x, y, styles) {
81918271
super();
81928272
for (let prop in proto20) {
@@ -8239,7 +8319,8 @@ var Two = (() => {
82398319
"visible",
82408320
"fill",
82418321
"stroke",
8242-
"dashes"
8322+
"dashes",
8323+
"strokeAttenuation"
82438324
];
82448325
/**
82458326
*
@@ -8353,7 +8434,7 @@ var Two = (() => {
83538434
* @function
83548435
* @returns {Two.Text}
83558436
* @description Release the text's renderer resources and detach all events.
8356-
* This method disposes fill and stroke effects (calling dispose() on
8437+
* This method disposes fill and stroke effects (calling dispose() on
83578438
* Gradients and Textures for thorough cleanup) while preserving the
83588439
* renderer type for potential re-attachment to a new renderer.
83598440
*/
@@ -8655,6 +8736,22 @@ var Two = (() => {
86558736
}
86568737
this._dashes = v;
86578738
}
8739+
},
8740+
/**
8741+
* @name Two.Text#strokeAttenuation
8742+
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
8743+
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
8744+
*/
8745+
strokeAttenuation: {
8746+
enumerable: true,
8747+
get: function() {
8748+
return this._strokeAttenuation;
8749+
},
8750+
set: function(v) {
8751+
this._strokeAttenuation = !!v;
8752+
this._flagStrokeAttenuation = true;
8753+
this._flagLinewidth = true;
8754+
}
86588755
}
86598756
};
86608757
function FlagFill2() {
@@ -9265,6 +9362,12 @@ var Two = (() => {
92659362
* @property {Two.Shape} - The Two.js object to clip from a group's rendering.
92669363
*/
92679364
_mask = null;
9365+
/**
9366+
* @name Two.Group#_strokeAttenuation
9367+
* @private
9368+
* @see {@link Two.Group#strokeAttenuation}
9369+
*/
9370+
_strokeAttenuation = true;
92689371
constructor(children) {
92699372
super();
92709373
for (let prop in proto22) {
@@ -9999,6 +10102,26 @@ var Two = (() => {
999910102
v.clip = true;
1000010103
}
1000110104
}
10105+
},
10106+
/**
10107+
* @name Two.Group#strokeAttenuation
10108+
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space for all child shapes.
10109+
* @description When `strokeAttenuation` is `false`, this property is applied to all child shapes, making their stroke widths automatically adjust to compensate for the group's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke widths scale normally with transformations.
10110+
*/
10111+
strokeAttenuation: {
10112+
enumerable: true,
10113+
get: function() {
10114+
return this._strokeAttenuation;
10115+
},
10116+
set: function(v) {
10117+
this._strokeAttenuation = !!v;
10118+
for (let i = 0; i < this.children.length; i++) {
10119+
const child = this.children[i];
10120+
if (child.strokeAttenuation !== void 0) {
10121+
child.strokeAttenuation = v;
10122+
}
10123+
}
10124+
}
1000210125
}
1000310126
};
1000410127
function replaceParent(child, newParent) {
@@ -11499,7 +11622,7 @@ var Two = (() => {
1149911622
ctx.strokeStyle = stroke._renderer.effect;
1150011623
}
1150111624
if (linewidth) {
11502-
ctx.lineWidth = linewidth;
11625+
ctx.lineWidth = getEffectiveStrokeWidth(this);
1150311626
}
1150411627
if (miter) {
1150511628
ctx.miterLimit = miter;
@@ -11695,7 +11818,7 @@ var Two = (() => {
1169511818
ctx.strokeStyle = stroke._renderer.effect;
1169611819
}
1169711820
if (linewidth) {
11698-
ctx.lineWidth = linewidth;
11821+
ctx.lineWidth = getEffectiveStrokeWidth(this);
1169911822
}
1170011823
}
1170111824
if (typeof opacity === "number") {
@@ -11831,7 +11954,7 @@ var Two = (() => {
1183111954
ctx.strokeStyle = stroke._renderer.effect;
1183211955
}
1183311956
if (linewidth) {
11834-
ctx.lineWidth = linewidth;
11957+
ctx.lineWidth = getEffectiveStrokeWidth(this);
1183511958
}
1183611959
}
1183711960
if (typeof opacity === "number") {
@@ -12574,7 +12697,7 @@ var Two = (() => {
1257412697
}
1257512698
}
1257612699
if (this._flagLinewidth) {
12577-
changed["stroke-width"] = this._linewidth;
12700+
changed["stroke-width"] = getEffectiveStrokeWidth(this);
1257812701
}
1257912702
if (this._flagOpacity) {
1258012703
changed["stroke-opacity"] = this._opacity;
@@ -12691,7 +12814,7 @@ var Two = (() => {
1269112814
}
1269212815
}
1269312816
if (this._flagLinewidth) {
12694-
changed["stroke-width"] = this._linewidth;
12817+
changed["stroke-width"] = getEffectiveStrokeWidth(this);
1269512818
}
1269612819
if (this._flagOpacity) {
1269712820
changed["stroke-opacity"] = this._opacity;
@@ -12785,7 +12908,7 @@ var Two = (() => {
1278512908
}
1278612909
}
1278712910
if (this._flagLinewidth) {
12788-
changed["stroke-width"] = this._linewidth;
12911+
changed["stroke-width"] = getEffectiveStrokeWidth(this);
1278912912
}
1279012913
if (this._flagOpacity) {
1279112914
changed.opacity = this._opacity;
@@ -13389,7 +13512,7 @@ var Two = (() => {
1338913512
ctx.strokeStyle = stroke._renderer.effect;
1339013513
}
1339113514
if (linewidth) {
13392-
ctx.lineWidth = linewidth;
13515+
ctx.lineWidth = getEffectiveStrokeWidth(elem);
1339313516
}
1339413517
if (miter) {
1339513518
ctx.miterLimit = miter;
@@ -13736,7 +13859,7 @@ var Two = (() => {
1373613859
ctx.strokeStyle = stroke._renderer.effect;
1373713860
}
1373813861
if (linewidth) {
13739-
ctx.lineWidth = linewidth / aspect;
13862+
ctx.lineWidth = getEffectiveStrokeWidth(elem) / aspect;
1374013863
}
1374113864
}
1374213865
if (typeof opacity === "number") {
@@ -13949,7 +14072,7 @@ var Two = (() => {
1394914072
ctx.strokeStyle = stroke._renderer.effect;
1395014073
}
1395114074
if (linewidth) {
13952-
ctx.lineWidth = linewidth;
14075+
ctx.lineWidth = getEffectiveStrokeWidth(elem);
1395314076
}
1395414077
}
1395514078
if (typeof opacity === "number") {

build/two.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)