Skip to content

Commit a603aa8

Browse files
authored
Merge pull request #789 from jonobr1/claude/issue-546-20250807-1706
feat: implement strokeUniform property for zoom-invariant stroke widths
2 parents f3bb781 + 812ce67 commit a603aa8

File tree

14 files changed

+331
-23
lines changed

14 files changed

+331
-23
lines changed

build/two.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1200,7 +1200,7 @@ var Two = (() => {
12001200
* @name Two.PublishDate
12011201
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
12021202
*/
1203-
PublishDate: "2025-09-30T22:09:35.343Z",
1203+
PublishDate: "2025-09-30T22:51:48.793Z",
12041204
/**
12051205
* @name Two.Identifier
12061206
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.

build/two.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/two.module.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,7 @@ var Constants = {
12081208
* @name Two.PublishDate
12091209
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
12101210
*/
1211-
PublishDate: "2025-09-30T22:09:35.343Z",
1211+
PublishDate: "2025-09-30T22:51:48.793Z",
12121212
/**
12131213
* @name Two.Identifier
12141214
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.

src/group.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ export class Group extends Shape {
197197
*/
198198
_mask = null;
199199

200+
/**
201+
* @name Two.Group#_strokeAttenuation
202+
* @private
203+
* @see {@link Two.Group#strokeAttenuation}
204+
*/
205+
_strokeAttenuation = true;
206+
200207
constructor(children) {
201208
super();
202209

@@ -1084,6 +1091,27 @@ const proto = {
10841091
}
10851092
},
10861093
},
1094+
1095+
/**
1096+
* @name Two.Group#strokeAttenuation
1097+
* @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.
1098+
* @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.
1099+
*/
1100+
strokeAttenuation: {
1101+
enumerable: true,
1102+
get: function () {
1103+
return this._strokeAttenuation;
1104+
},
1105+
set: function (v) {
1106+
this._strokeAttenuation = !!v;
1107+
for (let i = 0; i < this.children.length; i++) {
1108+
const child = this.children[i];
1109+
if (child.strokeAttenuation !== undefined) {
1110+
child.strokeAttenuation = v;
1111+
}
1112+
}
1113+
},
1114+
},
10871115
};
10881116

10891117
// /**

src/path.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ export class Path extends Shape {
115115
*/
116116
_flagMiter = true;
117117

118+
/**
119+
* @name Two.Path#_flagStrokeAttenuation
120+
* @private
121+
* @property {Boolean} - Determines whether the {@link Two.Path#strokeAttenuation} needs updating.
122+
*/
123+
_flagStrokeAttenuation = true;
124+
118125
/**
119126
* @name Two.Path#_flagMask
120127
* @private
@@ -250,6 +257,13 @@ export class Path extends Shape {
250257
*/
251258
_dashes = null;
252259

260+
/**
261+
* @name Two.Path#_strokeAttenuation
262+
* @private
263+
* @see {@link Two.Path#strokeAttenuation}
264+
*/
265+
_strokeAttenuation = true;
266+
253267
constructor(vertices, closed, curved, manual) {
254268
super();
255269

@@ -407,6 +421,7 @@ export class Path extends Shape {
407421
'beginning',
408422
'ending',
409423
'dashes',
424+
'strokeAttenuation',
410425
];
411426

412427
static Utils = {
@@ -1231,6 +1246,7 @@ export class Path extends Shape {
12311246
this._flagJoin =
12321247
this._flagMiter =
12331248
this._flagClip =
1249+
this._flagStrokeAttenuation =
12341250
false;
12351251

12361252
Shape.prototype.flagReset.call(this);
@@ -1516,6 +1532,23 @@ const proto = {
15161532
this._dashes = v;
15171533
},
15181534
},
1535+
1536+
/**
1537+
* @name Two.Path#strokeAttenuation
1538+
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
1539+
* @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.
1540+
*/
1541+
strokeAttenuation: {
1542+
enumerable: true,
1543+
get: function () {
1544+
return this._strokeAttenuation;
1545+
},
1546+
set: function (v) {
1547+
this._strokeAttenuation = !!v;
1548+
this._flagStrokeAttenuation = true;
1549+
this._flagLinewidth = true;
1550+
},
1551+
},
15191552
};
15201553

15211554
// Utility functions

src/renderers/canvas.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Commands } from '../utils/path-commands.js';
2-
import { decomposeMatrix, mod, TWO_PI } from '../utils/math.js';
2+
import { decomposeMatrix, mod, TWO_PI, getEffectiveStrokeWidth } from '../utils/math.js';
33
import { Curve } from '../utils/curves.js';
44
import { Events } from '../events.js';
55
import { getRatio } from '../utils/device-pixel-ratio.js';
@@ -232,7 +232,7 @@ const canvas = {
232232
ctx.strokeStyle = stroke._renderer.effect;
233233
}
234234
if (linewidth) {
235-
ctx.lineWidth = linewidth;
235+
ctx.lineWidth = getEffectiveStrokeWidth(this);
236236
}
237237
if (miter) {
238238
ctx.miterLimit = miter;
@@ -490,7 +490,7 @@ const canvas = {
490490
ctx.strokeStyle = stroke._renderer.effect;
491491
}
492492
if (linewidth) {
493-
ctx.lineWidth = linewidth;
493+
ctx.lineWidth = getEffectiveStrokeWidth(this);
494494
}
495495
}
496496
if (typeof opacity === 'number') {
@@ -664,7 +664,7 @@ const canvas = {
664664
ctx.strokeStyle = stroke._renderer.effect;
665665
}
666666
if (linewidth) {
667-
ctx.lineWidth = linewidth;
667+
ctx.lineWidth = getEffectiveStrokeWidth(this);
668668
}
669669
}
670670
if (typeof opacity === 'number') {

src/renderers/svg.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Commands } from '../utils/path-commands.js';
2-
import { decomposeMatrix, mod, toFixed } from '../utils/math.js';
2+
import { decomposeMatrix, mod, toFixed, getEffectiveStrokeWidth } from '../utils/math.js';
33
import { Events } from '../events.js';
44
import { _ } from '../utils/underscore.js';
55

@@ -505,7 +505,7 @@ const svg = {
505505
}
506506

507507
if (this._flagLinewidth) {
508-
changed['stroke-width'] = this._linewidth;
508+
changed['stroke-width'] = getEffectiveStrokeWidth(this);
509509
}
510510

511511
if (this._flagOpacity) {
@@ -672,7 +672,7 @@ const svg = {
672672
}
673673

674674
if (this._flagLinewidth) {
675-
changed['stroke-width'] = this._linewidth;
675+
changed['stroke-width'] = getEffectiveStrokeWidth(this);
676676
}
677677

678678
if (this._flagOpacity) {
@@ -797,7 +797,7 @@ const svg = {
797797
}
798798
}
799799
if (this._flagLinewidth) {
800-
changed['stroke-width'] = this._linewidth;
800+
changed['stroke-width'] = getEffectiveStrokeWidth(this);
801801
}
802802
if (this._flagOpacity) {
803803
changed.opacity = this._opacity;

src/renderers/webgl.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Commands } from '../utils/path-commands.js';
22

33
import { root } from '../utils/root.js';
4-
import { getPoT, mod, NumArray, TWO_PI } from '../utils/math.js';
4+
import { getPoT, mod, NumArray, TWO_PI, getEffectiveStrokeWidth } from '../utils/math.js';
55
import { shaders } from '../utils/shaders.js';
66
import { Events } from '../events.js';
77
import { TwoError } from '../utils/error.js';
@@ -235,7 +235,7 @@ const webgl = {
235235
ctx.strokeStyle = stroke._renderer.effect;
236236
}
237237
if (linewidth) {
238-
ctx.lineWidth = linewidth;
238+
ctx.lineWidth = getEffectiveStrokeWidth(elem);
239239
}
240240
if (miter) {
241241
ctx.miterLimit = miter;
@@ -731,7 +731,7 @@ const webgl = {
731731
ctx.strokeStyle = stroke._renderer.effect;
732732
}
733733
if (linewidth) {
734-
ctx.lineWidth = linewidth / aspect;
734+
ctx.lineWidth = getEffectiveStrokeWidth(elem) / aspect;
735735
}
736736
}
737737
if (typeof opacity === 'number') {
@@ -1039,7 +1039,7 @@ const webgl = {
10391039
ctx.strokeStyle = stroke._renderer.effect;
10401040
}
10411041
if (linewidth) {
1042-
ctx.lineWidth = linewidth;
1042+
ctx.lineWidth = getEffectiveStrokeWidth(elem);
10431043
}
10441044
}
10451045
if (typeof opacity === 'number') {

src/shapes/points.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class Points extends Shape {
4141
_flagVisible = true;
4242
_flagSize = true;
4343
_flagSizeAttenuation = true;
44+
_flagStrokeAttenuation = true;
4445

4546
_length = 0;
4647
_fill = '#fff';
@@ -53,6 +54,7 @@ export class Points extends Shape {
5354
_beginning = 0;
5455
_ending = 1.0;
5556
_dashes = null;
57+
_strokeAttenuation = true;
5658

5759
constructor(vertices) {
5860
super();
@@ -180,6 +182,7 @@ export class Points extends Shape {
180182
'beginning',
181183
'ending',
182184
'dashes',
185+
'strokeAttenuation',
183186
];
184187

185188
/**
@@ -702,4 +705,21 @@ const proto = {
702705
this._dashes = v;
703706
},
704707
},
708+
709+
/**
710+
* @name Two.Points#strokeAttenuation
711+
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
712+
* @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.
713+
*/
714+
strokeAttenuation: {
715+
enumerable: true,
716+
get: function () {
717+
return this._strokeAttenuation;
718+
},
719+
set: function (v) {
720+
this._strokeAttenuation = !!v;
721+
this._flagStrokeAttenuation = true;
722+
this._flagLinewidth = true;
723+
},
724+
},
705725
};

src/text.js

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ export class Text extends Shape {
129129
_flagVisible = true;
130130

131131
/**
132-
* @name Two.Path#_flagMask
132+
* @name Two.Text#_flagMask
133133
* @private
134-
* @property {Boolean} - Determines whether the {@link Two.Path#mask} needs updating.
134+
* @property {Boolean} - Determines whether the {@link Two.Text#mask} needs updating.
135135
*/
136136
_flagMask = false;
137137

@@ -149,6 +149,13 @@ export class Text extends Shape {
149149
*/
150150
_flagDirection = true;
151151

152+
/**
153+
* @name Two.Text#_flagStrokeAttenuation
154+
* @private
155+
* @property {Boolean} - Determines whether the {@link Two.Text#strokeAttenuation} needs updating.
156+
*/
157+
_flagStrokeAttenuation = true;
158+
152159
// Underlying Properties
153160

154161
/**
@@ -267,6 +274,13 @@ export class Text extends Shape {
267274
*/
268275
_dashes = null;
269276

277+
/**
278+
* @name Two.Text#_strokeAttenuation
279+
* @private
280+
* @see {@link Two.Text#strokeAttenuation}
281+
*/
282+
_strokeAttenuation = true;
283+
270284
constructor(message, x, y, styles) {
271285
super();
272286

@@ -340,6 +354,7 @@ export class Text extends Shape {
340354
'fill',
341355
'stroke',
342356
'dashes',
357+
'strokeAttenuation',
343358
];
344359

345360
/**
@@ -474,28 +489,34 @@ export class Text extends Shape {
474489
* @function
475490
* @returns {Two.Text}
476491
* @description Release the text's renderer resources and detach all events.
477-
* This method disposes fill and stroke effects (calling dispose() on
478492
* Gradients and Textures for thorough cleanup) while preserving the
479493
* renderer type for potential re-attachment to a new renderer.
480494
*/
481495
dispose() {
482496
// Call parent dispose to preserve renderer type and unbind events
483497
super.dispose();
484-
485498
// Dispose fill effect (more thorough than unbind)
486499
if (typeof this.fill === 'object' && this.fill && 'dispose' in this.fill) {
487500
this.fill.dispose();
488501
} else if (typeof this.fill === 'object' && this.fill && 'unbind' in this.fill) {
502+
) {
489503
this.fill.unbind();
490504
}
491505

492506
// Dispose stroke effect (more thorough than unbind)
493507
if (typeof this.stroke === 'object' && this.stroke && 'dispose' in this.stroke) {
508+
if (
509+
typeof this.stroke === 'object' &&
510+
) {
494511
this.stroke.dispose();
495-
} else if (typeof this.stroke === 'object' && this.stroke && 'unbind' in this.stroke) {
512+
} else if (
513+
typeof this.stroke === 'object' &&
514+
this.stroke &&
515+
'unbind' in this.stroke
516+
) {
496517
this.stroke.unbind();
497518
}
498-
519+
499520
return this;
500521
}
501522

@@ -841,6 +862,22 @@ const proto = {
841862
this._dashes = v;
842863
},
843864
},
865+
/**
866+
* @name Two.Text#strokeAttenuation
867+
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
868+
* @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.
869+
*/
870+
strokeAttenuation: {
871+
enumerable: true,
872+
get: function () {
873+
return this._strokeAttenuation;
874+
},
875+
set: function (v) {
876+
this._strokeAttenuation = !!v;
877+
this._flagStrokeAttenuation = true;
878+
this._flagLinewidth = true;
879+
},
880+
},
844881
};
845882

846883
/**

0 commit comments

Comments
 (0)