etro 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +6 -3
- package/CHANGELOG.md +11 -0
- package/CONTRIBUTING.md +6 -7
- package/dist/etro-cjs.js +36 -4
- package/dist/etro-iife.js +36 -4
- package/dist/layer/text.d.ts +16 -1
- package/package.json +6 -5
- package/ship.config.js +12 -1
- package/src/layer/audio-source.ts +6 -3
- package/src/layer/text.ts +48 -2
package/.husky/pre-commit
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.12.0] - 2024-01-15
|
|
9
|
+
### Added
|
|
10
|
+
- `stroke` option for `Text` layer ([#239](https://github.com/etro-js/etro/pull/239)).
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- Bump @babel/traverse from 7.18.13 to 7.23.2 ([#244](https://github.com/etro-js/etro/pull/244)).
|
|
14
|
+
- Bump browserify-sign from 4.2.1 to 4.2.2 ([#245](https://github.com/etro-js/etro/pull/245)).
|
|
15
|
+
- Bump follow-redirects from 1.14.9 to 1.15.4 ([#247](https://github.com/etro-js/etro/pull/247)).
|
|
16
|
+
- Bump gitmoji-cli from 8.4.0 to 9.0.0.
|
|
17
|
+
|
|
8
18
|
## [0.11.0] - 2023-08-05
|
|
9
19
|
### Added
|
|
10
20
|
- `duration` option for `Movie#play` ([#208](https://github.com/etro-js/etro/pull/208)).
|
|
@@ -292,6 +302,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
292
302
|
- Gaussian blur
|
|
293
303
|
- Transform
|
|
294
304
|
|
|
305
|
+
[0.12.0]: https://github.com/etro-js/etro/compare/v0.11.0...v0.12.0
|
|
295
306
|
[0.11.0]: https://github.com/etro-js/etro/compare/v0.10.1...v0.11.0
|
|
296
307
|
[0.10.1]: https://github.com/etro-js/etro/compare/v0.10.0...v0.10.1
|
|
297
308
|
[0.10.0]: https://github.com/etro-js/etro/compare/v0.9.1...v0.10.0
|
package/CONTRIBUTING.md
CHANGED
|
@@ -33,13 +33,12 @@ Thank you for considering contributing to Etro! There are many ways you can cont
|
|
|
33
33
|
```
|
|
34
34
|
npm run fix
|
|
35
35
|
npm run build
|
|
36
|
-
npm test:unit
|
|
37
|
-
npm test:smoke
|
|
38
|
-
npm test:integration
|
|
36
|
+
npm run test:unit
|
|
37
|
+
npm run test:smoke
|
|
38
|
+
npm run test:integration
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
to lint and compile the code and run the tests on them. Husky will run these commands automatically when you commit.
|
|
42
|
-
- *Note: Unit tests validate the logic of the code in etro, with the DOM and any other external dependencies mocked. Because audio cannot be captured in the GitHub Actions runner, the end-to-end tests are divided into two suites. All end-to-end tests that need to validate audio output should be placed in **spec/integration/**. All end-to-end tests that do **not** require an audio device should be placed in **spec/smoke/**. The integration tests can only be run locally, but the other two suites can be run anywhere.*
|
|
43
42
|
|
|
44
43
|
- Please commit to a new branch, not master
|
|
45
44
|
|
|
@@ -53,11 +52,11 @@ Thank you for considering contributing to Etro! There are many ways you can cont
|
|
|
53
52
|
- Open a pull request from the branch in your fork to the main repository. If you changed any core functionality, make sure you explain your motives for those changes
|
|
54
53
|
- A large part of the submission process is receiving feedback on how you can improve you pull request. If you need to change your pull request, feel free to push more commits.
|
|
55
54
|
|
|
56
|
-
##
|
|
55
|
+
## Tests
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
Automated tests are run with [KarmaJS](https://karma-runner.github.io/) in a headless Firefox session. Unit tests validate the logic of the code in etro, with the DOM and any other external dependencies mocked. Because audio cannot be captured in the GitHub Actions runner, the end-to-end tests are divided into two suites. All end-to-end tests that need to validate audio output should be placed in **spec/integration/**. All end-to-end tests that do **not** require an audio device should be placed in **spec/smoke/**. The integration tests can only be run locally, but the other two suites can be run anywhere.
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
## Code overview
|
|
61
60
|
|
|
62
61
|
### Events
|
|
63
62
|
|
package/dist/etro-cjs.js
CHANGED
|
@@ -667,9 +667,11 @@ function AudioSourceMixin(superclass) {
|
|
|
667
667
|
_this._sourceStartTime = options.sourceStartTime || 0;
|
|
668
668
|
applyOptions(options, _this);
|
|
669
669
|
var load = function () {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
670
|
+
if (options.duration < 0) {
|
|
671
|
+
throw new Error('Invalid options.duration. It must be a non-negative value.');
|
|
672
|
+
}
|
|
673
|
+
if (_this.sourceStartTime > _this.source.duration) {
|
|
674
|
+
throw new Error('options.sourceStartTime cannot exceed options.source.duration');
|
|
673
675
|
}
|
|
674
676
|
_this._unstretchedDuration = options.duration || (_this.source.duration - _this.sourceStartTime);
|
|
675
677
|
_this.duration = _this._unstretchedDuration / (_this.playbackRate);
|
|
@@ -1432,6 +1434,12 @@ var Image = /** @class */ (function (_super) {
|
|
|
1432
1434
|
return Image;
|
|
1433
1435
|
}(VisualSourceMixin(Visual)));
|
|
1434
1436
|
|
|
1437
|
+
var TextStrokePosition;
|
|
1438
|
+
(function (TextStrokePosition) {
|
|
1439
|
+
TextStrokePosition[TextStrokePosition["Inside"] = 0] = "Inside";
|
|
1440
|
+
TextStrokePosition[TextStrokePosition["Center"] = 1] = "Center";
|
|
1441
|
+
TextStrokePosition[TextStrokePosition["Outside"] = 2] = "Outside";
|
|
1442
|
+
})(TextStrokePosition || (TextStrokePosition = {}));
|
|
1435
1443
|
var Text = /** @class */ (function (_super) {
|
|
1436
1444
|
__extends(Text, _super);
|
|
1437
1445
|
/**
|
|
@@ -1456,6 +1464,7 @@ var Text = /** @class */ (function (_super) {
|
|
|
1456
1464
|
// this._prevMaxWidth = undefined;
|
|
1457
1465
|
}
|
|
1458
1466
|
Text.prototype.doRender = function () {
|
|
1467
|
+
var _a, _b;
|
|
1459
1468
|
_super.prototype.doRender.call(this);
|
|
1460
1469
|
var text = val(this, 'text', this.currentTime);
|
|
1461
1470
|
var font = val(this, 'font', this.currentTime);
|
|
@@ -1469,6 +1478,28 @@ var Text = /** @class */ (function (_super) {
|
|
|
1469
1478
|
this.cctx.textBaseline = val(this, 'textBaseline', this.currentTime);
|
|
1470
1479
|
this.cctx.direction = val(this, 'textDirection', this.currentTime);
|
|
1471
1480
|
this.cctx.fillText(text, val(this, 'textX', this.currentTime), val(this, 'textY', this.currentTime), maxWidth);
|
|
1481
|
+
var textStroke = val(this, 'textStroke', this.currentTime);
|
|
1482
|
+
if (textStroke) {
|
|
1483
|
+
this.cctx.strokeStyle = textStroke.color;
|
|
1484
|
+
this.cctx.lineWidth = (_a = textStroke.thickness) !== null && _a !== void 0 ? _a : 1;
|
|
1485
|
+
var position = (_b = textStroke.position) !== null && _b !== void 0 ? _b : 'outer';
|
|
1486
|
+
// Save the globalCompositeOperation, we have to revert it after stroking the text.
|
|
1487
|
+
var globalCompositionOperation = this.cctx.globalCompositeOperation;
|
|
1488
|
+
switch (position) {
|
|
1489
|
+
case TextStrokePosition.Inside:
|
|
1490
|
+
this.cctx.globalCompositeOperation = 'source-atop';
|
|
1491
|
+
this.cctx.lineWidth *= 2;
|
|
1492
|
+
break;
|
|
1493
|
+
case TextStrokePosition.Center:
|
|
1494
|
+
break;
|
|
1495
|
+
case TextStrokePosition.Outside:
|
|
1496
|
+
this.cctx.globalCompositeOperation = 'destination-over';
|
|
1497
|
+
this.cctx.lineWidth *= 2;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
this.cctx.strokeText(text, val(this, 'textX', this.currentTime), val(this, 'textY', this.currentTime), maxWidth);
|
|
1501
|
+
this.cctx.globalCompositeOperation = globalCompositionOperation;
|
|
1502
|
+
}
|
|
1472
1503
|
this._prevText = text;
|
|
1473
1504
|
this._prevFont = font;
|
|
1474
1505
|
this._prevMaxWidth = maxWidth;
|
|
@@ -1497,7 +1528,7 @@ var Text = /** @class */ (function (_super) {
|
|
|
1497
1528
|
* @deprecated See {@link https://github.com/etro-js/etro/issues/131}
|
|
1498
1529
|
*/
|
|
1499
1530
|
Text.prototype.getDefaultOptions = function () {
|
|
1500
|
-
return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: parseColor('#fff'), textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr' });
|
|
1531
|
+
return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: parseColor('#fff'), textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr', textStroke: null });
|
|
1501
1532
|
};
|
|
1502
1533
|
return Text;
|
|
1503
1534
|
}(Visual));
|
|
@@ -1535,6 +1566,7 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
1535
1566
|
Base: Base,
|
|
1536
1567
|
Image: Image,
|
|
1537
1568
|
Text: Text,
|
|
1569
|
+
get TextStrokePosition () { return TextStrokePosition; },
|
|
1538
1570
|
Video: Video,
|
|
1539
1571
|
VisualSourceMixin: VisualSourceMixin,
|
|
1540
1572
|
Visual: Visual
|
package/dist/etro-iife.js
CHANGED
|
@@ -668,9 +668,11 @@ var etro = (function () {
|
|
|
668
668
|
_this._sourceStartTime = options.sourceStartTime || 0;
|
|
669
669
|
applyOptions(options, _this);
|
|
670
670
|
var load = function () {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
671
|
+
if (options.duration < 0) {
|
|
672
|
+
throw new Error('Invalid options.duration. It must be a non-negative value.');
|
|
673
|
+
}
|
|
674
|
+
if (_this.sourceStartTime > _this.source.duration) {
|
|
675
|
+
throw new Error('options.sourceStartTime cannot exceed options.source.duration');
|
|
674
676
|
}
|
|
675
677
|
_this._unstretchedDuration = options.duration || (_this.source.duration - _this.sourceStartTime);
|
|
676
678
|
_this.duration = _this._unstretchedDuration / (_this.playbackRate);
|
|
@@ -1433,6 +1435,12 @@ var etro = (function () {
|
|
|
1433
1435
|
return Image;
|
|
1434
1436
|
}(VisualSourceMixin(Visual)));
|
|
1435
1437
|
|
|
1438
|
+
var TextStrokePosition;
|
|
1439
|
+
(function (TextStrokePosition) {
|
|
1440
|
+
TextStrokePosition[TextStrokePosition["Inside"] = 0] = "Inside";
|
|
1441
|
+
TextStrokePosition[TextStrokePosition["Center"] = 1] = "Center";
|
|
1442
|
+
TextStrokePosition[TextStrokePosition["Outside"] = 2] = "Outside";
|
|
1443
|
+
})(TextStrokePosition || (TextStrokePosition = {}));
|
|
1436
1444
|
var Text = /** @class */ (function (_super) {
|
|
1437
1445
|
__extends(Text, _super);
|
|
1438
1446
|
/**
|
|
@@ -1457,6 +1465,7 @@ var etro = (function () {
|
|
|
1457
1465
|
// this._prevMaxWidth = undefined;
|
|
1458
1466
|
}
|
|
1459
1467
|
Text.prototype.doRender = function () {
|
|
1468
|
+
var _a, _b;
|
|
1460
1469
|
_super.prototype.doRender.call(this);
|
|
1461
1470
|
var text = val(this, 'text', this.currentTime);
|
|
1462
1471
|
var font = val(this, 'font', this.currentTime);
|
|
@@ -1470,6 +1479,28 @@ var etro = (function () {
|
|
|
1470
1479
|
this.cctx.textBaseline = val(this, 'textBaseline', this.currentTime);
|
|
1471
1480
|
this.cctx.direction = val(this, 'textDirection', this.currentTime);
|
|
1472
1481
|
this.cctx.fillText(text, val(this, 'textX', this.currentTime), val(this, 'textY', this.currentTime), maxWidth);
|
|
1482
|
+
var textStroke = val(this, 'textStroke', this.currentTime);
|
|
1483
|
+
if (textStroke) {
|
|
1484
|
+
this.cctx.strokeStyle = textStroke.color;
|
|
1485
|
+
this.cctx.lineWidth = (_a = textStroke.thickness) !== null && _a !== void 0 ? _a : 1;
|
|
1486
|
+
var position = (_b = textStroke.position) !== null && _b !== void 0 ? _b : 'outer';
|
|
1487
|
+
// Save the globalCompositeOperation, we have to revert it after stroking the text.
|
|
1488
|
+
var globalCompositionOperation = this.cctx.globalCompositeOperation;
|
|
1489
|
+
switch (position) {
|
|
1490
|
+
case TextStrokePosition.Inside:
|
|
1491
|
+
this.cctx.globalCompositeOperation = 'source-atop';
|
|
1492
|
+
this.cctx.lineWidth *= 2;
|
|
1493
|
+
break;
|
|
1494
|
+
case TextStrokePosition.Center:
|
|
1495
|
+
break;
|
|
1496
|
+
case TextStrokePosition.Outside:
|
|
1497
|
+
this.cctx.globalCompositeOperation = 'destination-over';
|
|
1498
|
+
this.cctx.lineWidth *= 2;
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
this.cctx.strokeText(text, val(this, 'textX', this.currentTime), val(this, 'textY', this.currentTime), maxWidth);
|
|
1502
|
+
this.cctx.globalCompositeOperation = globalCompositionOperation;
|
|
1503
|
+
}
|
|
1473
1504
|
this._prevText = text;
|
|
1474
1505
|
this._prevFont = font;
|
|
1475
1506
|
this._prevMaxWidth = maxWidth;
|
|
@@ -1498,7 +1529,7 @@ var etro = (function () {
|
|
|
1498
1529
|
* @deprecated See {@link https://github.com/etro-js/etro/issues/131}
|
|
1499
1530
|
*/
|
|
1500
1531
|
Text.prototype.getDefaultOptions = function () {
|
|
1501
|
-
return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: parseColor('#fff'), textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr' });
|
|
1532
|
+
return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: parseColor('#fff'), textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr', textStroke: null });
|
|
1502
1533
|
};
|
|
1503
1534
|
return Text;
|
|
1504
1535
|
}(Visual));
|
|
@@ -1536,6 +1567,7 @@ var etro = (function () {
|
|
|
1536
1567
|
Base: Base,
|
|
1537
1568
|
Image: Image,
|
|
1538
1569
|
Text: Text,
|
|
1570
|
+
get TextStrokePosition () { return TextStrokePosition; },
|
|
1539
1571
|
Video: Video,
|
|
1540
1572
|
VisualSourceMixin: VisualSourceMixin,
|
|
1541
1573
|
Visual: Visual
|
package/dist/layer/text.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Dynamic, Color } from '../util';
|
|
2
2
|
import { Visual, VisualOptions } from './visual';
|
|
3
|
+
declare enum TextStrokePosition {
|
|
4
|
+
Inside = 0,
|
|
5
|
+
Center = 1,
|
|
6
|
+
Outside = 2
|
|
7
|
+
}
|
|
3
8
|
interface TextOptions extends VisualOptions {
|
|
4
9
|
text: Dynamic<string>;
|
|
5
10
|
font?: Dynamic<string>;
|
|
@@ -23,6 +28,11 @@ interface TextOptions extends VisualOptions {
|
|
|
23
28
|
* @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
24
29
|
*/
|
|
25
30
|
textDirection?: Dynamic<string>;
|
|
31
|
+
textStroke?: Dynamic<{
|
|
32
|
+
color: Color;
|
|
33
|
+
position?: TextStrokePosition;
|
|
34
|
+
thickness?: number;
|
|
35
|
+
}>;
|
|
26
36
|
}
|
|
27
37
|
declare class Text extends Visual {
|
|
28
38
|
text: Dynamic<string>;
|
|
@@ -47,6 +57,11 @@ declare class Text extends Visual {
|
|
|
47
57
|
* @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
48
58
|
*/
|
|
49
59
|
textDirection: Dynamic<string>;
|
|
60
|
+
textStroke?: Dynamic<{
|
|
61
|
+
color: Color;
|
|
62
|
+
position?: TextStrokePosition;
|
|
63
|
+
thickness?: number;
|
|
64
|
+
}>;
|
|
50
65
|
private _prevText;
|
|
51
66
|
private _prevFont;
|
|
52
67
|
private _prevMaxWidth;
|
|
@@ -60,4 +75,4 @@ declare class Text extends Visual {
|
|
|
60
75
|
*/
|
|
61
76
|
getDefaultOptions(): TextOptions;
|
|
62
77
|
}
|
|
63
|
-
export { Text, TextOptions };
|
|
78
|
+
export { Text, TextOptions, TextStrokePosition };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "etro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "An extendable video-editing framework for the browser",
|
|
5
5
|
"browser": "dist/etro-cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
|
18
18
|
"@typescript-eslint/parser": "^5.30.7",
|
|
19
19
|
"canvas": "^2.11.2",
|
|
20
|
+
"cross-env": "^7.0.3",
|
|
20
21
|
"docdash": "^1.1.1",
|
|
21
22
|
"ecstatic": ">=4.1.3",
|
|
22
23
|
"eslint": "^8.20.0",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"eslint-plugin-promise": "^6.0.0",
|
|
28
29
|
"eslint-plugin-standard": "^5.0.0",
|
|
29
30
|
"ev": "0.0.7",
|
|
30
|
-
"gitmoji-cli": "^
|
|
31
|
+
"gitmoji-cli": "^9.0.0",
|
|
31
32
|
"http-server": "^14.1.1",
|
|
32
33
|
"husky": "^8.0.3",
|
|
33
34
|
"jasmine": "^3.4.0",
|
|
@@ -60,9 +61,9 @@
|
|
|
60
61
|
"lint:test": "eslint -c eslint.test-conf.js --ext .ts spec",
|
|
61
62
|
"lint:examples": "eslint -c eslint.example-conf.js --ext .html examples",
|
|
62
63
|
"start": "http-server",
|
|
63
|
-
"test:unit": "TEST_SUITE=unit karma start",
|
|
64
|
-
"test:smoke": "TEST_SUITE=smoke karma start",
|
|
65
|
-
"test:integration": "TEST_SUITE=integration karma start",
|
|
64
|
+
"test:unit": "cross-env TEST_SUITE=unit karma start",
|
|
65
|
+
"test:smoke": "cross-env TEST_SUITE=smoke karma start",
|
|
66
|
+
"test:integration": "cross-env TEST_SUITE=integration karma start",
|
|
66
67
|
"release": "shipjs prepare"
|
|
67
68
|
},
|
|
68
69
|
"repository": {
|
package/ship.config.js
CHANGED
|
@@ -24,7 +24,18 @@ module.exports = {
|
|
|
24
24
|
release.setDate(new Date()) // today
|
|
25
25
|
const newChangelog = parsed.toString()
|
|
26
26
|
fs.writeFileSync(changelogFile, newChangelog, 'utf8')
|
|
27
|
-
}
|
|
27
|
+
},
|
|
28
|
+
releases: [
|
|
29
|
+
{
|
|
30
|
+
extractChangelog: ({ version, dir }) => {
|
|
31
|
+
const changelogFile = `${dir}/CHANGELOG.md`
|
|
32
|
+
const changelog = fs.readFileSync(changelogFile, 'utf8')
|
|
33
|
+
const parsed = parser(changelog)
|
|
34
|
+
const release = parsed.findRelease(version)
|
|
35
|
+
return `${release.toString()}\n${release.getCompareLink()}`
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
]
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
class Changelog {
|
|
@@ -85,9 +85,12 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
85
85
|
applyOptions(options, this)
|
|
86
86
|
|
|
87
87
|
const load = () => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
if (options.duration < 0) {
|
|
89
|
+
throw new Error('Invalid options.duration. It must be a non-negative value.')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.sourceStartTime > this.source.duration) {
|
|
93
|
+
throw new Error('options.sourceStartTime cannot exceed options.source.duration')
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
this._unstretchedDuration = options.duration || (this.source.duration - this.sourceStartTime)
|
package/src/layer/text.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Dynamic, val, applyOptions, Color, parseColor } from '../util'
|
|
2
2
|
import { Visual, VisualOptions } from './visual'
|
|
3
3
|
|
|
4
|
+
enum TextStrokePosition {
|
|
5
|
+
Inside,
|
|
6
|
+
Center,
|
|
7
|
+
Outside,
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
interface TextOptions extends VisualOptions {
|
|
5
11
|
text: Dynamic<string>
|
|
6
12
|
font?: Dynamic<string>
|
|
@@ -24,6 +30,12 @@ interface TextOptions extends VisualOptions {
|
|
|
24
30
|
* @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
25
31
|
*/
|
|
26
32
|
textDirection?: Dynamic<string>
|
|
33
|
+
|
|
34
|
+
textStroke?: Dynamic<{
|
|
35
|
+
color: Color,
|
|
36
|
+
position?: TextStrokePosition
|
|
37
|
+
thickness?: number
|
|
38
|
+
}>
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
class Text extends Visual {
|
|
@@ -49,6 +61,11 @@ class Text extends Visual {
|
|
|
49
61
|
* @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
50
62
|
*/
|
|
51
63
|
textDirection: Dynamic<string>
|
|
64
|
+
textStroke?: Dynamic<{
|
|
65
|
+
color: Color,
|
|
66
|
+
position?: TextStrokePosition
|
|
67
|
+
thickness?: number
|
|
68
|
+
}>
|
|
52
69
|
|
|
53
70
|
private _prevText: string
|
|
54
71
|
private _prevFont: string
|
|
@@ -94,6 +111,34 @@ class Text extends Visual {
|
|
|
94
111
|
maxWidth
|
|
95
112
|
)
|
|
96
113
|
|
|
114
|
+
const textStroke = val(this, 'textStroke', this.currentTime)
|
|
115
|
+
if (textStroke) {
|
|
116
|
+
this.cctx.strokeStyle = textStroke.color
|
|
117
|
+
this.cctx.lineWidth = textStroke.thickness ?? 1
|
|
118
|
+
const position = textStroke.position ?? 'outer'
|
|
119
|
+
// Save the globalCompositeOperation, we have to revert it after stroking the text.
|
|
120
|
+
const globalCompositionOperation = this.cctx.globalCompositeOperation
|
|
121
|
+
switch (position) {
|
|
122
|
+
case TextStrokePosition.Inside:
|
|
123
|
+
this.cctx.globalCompositeOperation = 'source-atop'
|
|
124
|
+
this.cctx.lineWidth *= 2
|
|
125
|
+
break
|
|
126
|
+
case TextStrokePosition.Center:
|
|
127
|
+
break
|
|
128
|
+
case TextStrokePosition.Outside:
|
|
129
|
+
this.cctx.globalCompositeOperation = 'destination-over'
|
|
130
|
+
this.cctx.lineWidth *= 2
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
this.cctx.strokeText(
|
|
134
|
+
text,
|
|
135
|
+
val(this, 'textX', this.currentTime),
|
|
136
|
+
val(this, 'textY', this.currentTime),
|
|
137
|
+
maxWidth
|
|
138
|
+
)
|
|
139
|
+
this.cctx.globalCompositeOperation = globalCompositionOperation
|
|
140
|
+
}
|
|
141
|
+
|
|
97
142
|
this._prevText = text
|
|
98
143
|
this._prevFont = font
|
|
99
144
|
this._prevMaxWidth = maxWidth
|
|
@@ -137,9 +182,10 @@ class Text extends Visual {
|
|
|
137
182
|
maxWidth: null,
|
|
138
183
|
textAlign: 'start',
|
|
139
184
|
textBaseline: 'top',
|
|
140
|
-
textDirection: 'ltr'
|
|
185
|
+
textDirection: 'ltr',
|
|
186
|
+
textStroke: null
|
|
141
187
|
}
|
|
142
188
|
}
|
|
143
189
|
}
|
|
144
190
|
|
|
145
|
-
export { Text, TextOptions }
|
|
191
|
+
export { Text, TextOptions, TextStrokePosition }
|