@remotion/rounded-text-box 4.0.361

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/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @remotion/rounded-text-box
2
+
3
+ Create a TikTok-like multiline text box SVG path with rounded corners
4
+
5
+ [![NPM Downloads](https://img.shields.io/npm/dm/@remotion/rounded-text-box.svg?style=flat&color=black&label=Downloads)](https://npmcharts.com/compare/@remotion/rounded-text-box?minimal=true)
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @remotion/rounded-text-box --save-exact
11
+ ```
12
+
13
+ When installing a Remotion package, make sure to align the version of all `remotion` and `@remotion/*` packages to the same version.
14
+ Remove the `^` character from the version number to use the exact version.
15
+
16
+ ## Usage
17
+
18
+ See the [documentation](https://www.remotion.dev/docs/rounded-text-box) for more information.
package/bundle.ts ADDED
@@ -0,0 +1,15 @@
1
+ import {buildPackage} from '../.monorepo/builder';
2
+
3
+ await buildPackage({
4
+ formats: {
5
+ cjs: 'build',
6
+ esm: 'build',
7
+ },
8
+ external: ['@remotion/layout-utils', '@remotion/paths'],
9
+ entrypoints: [
10
+ {
11
+ path: 'src/index.ts',
12
+ target: 'node',
13
+ },
14
+ ],
15
+ });
@@ -0,0 +1,19 @@
1
+ import type { Dimensions } from '@remotion/layout-utils';
2
+ import type { BoundingBox, ReducedInstruction } from '@remotion/paths';
3
+ export type TextAlign = 'left' | 'center' | 'right';
4
+ export type CreateRoundedTextBoxProps = {
5
+ textMeasurements: Dimensions[];
6
+ textAlign: TextAlign;
7
+ horizontalPadding: number;
8
+ borderRadius: number;
9
+ };
10
+ export type CreateRoundedTextBoxResult = {
11
+ d: string;
12
+ boundingBox: BoundingBox;
13
+ instructions: ReducedInstruction[];
14
+ };
15
+ export declare const createRoundedTextBox: ({ textMeasurements, textAlign, horizontalPadding, borderRadius: unclampedMaxCornerRadius, }: CreateRoundedTextBoxProps) => {
16
+ d: string;
17
+ boundingBox: BoundingBox;
18
+ instructions: ReducedInstruction[];
19
+ };
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRoundedTextBox = void 0;
4
+ const paths_1 = require("@remotion/paths");
5
+ const clamp = (val, min, max) => {
6
+ return Math.min(Math.max(val, min), max);
7
+ };
8
+ const createRoundedTextBox = ({ textMeasurements, textAlign, horizontalPadding, borderRadius: unclampedMaxCornerRadius, }) => {
9
+ const instructions = [];
10
+ let maxWidth = 0;
11
+ for (const cornerRounding of textMeasurements) {
12
+ maxWidth = Math.max(maxWidth, cornerRounding.width + horizontalPadding * 2);
13
+ }
14
+ let yOffset = 0;
15
+ for (let i = 0; i < textMeasurements.length; i++) {
16
+ const previousLine = textMeasurements[i - 1];
17
+ const currentLine = textMeasurements[i];
18
+ const nextLine = textMeasurements[i + 1];
19
+ let xOffset = 0;
20
+ if (textAlign === 'center') {
21
+ xOffset = (maxWidth - (currentLine.width + horizontalPadding * 2)) / 2;
22
+ }
23
+ else if (textAlign === 'right') {
24
+ xOffset = maxWidth - (currentLine.width + horizontalPadding * 2);
25
+ }
26
+ const maxCornerRadius = clamp(unclampedMaxCornerRadius, 0, currentLine.height / 2);
27
+ if (i === 0) {
28
+ instructions.push({
29
+ type: 'M',
30
+ x: xOffset + maxCornerRadius,
31
+ y: yOffset,
32
+ });
33
+ }
34
+ const topRightCornerRadius = clamp(previousLine
35
+ ? textAlign === 'right'
36
+ ? 0
37
+ : textAlign === 'left'
38
+ ? (previousLine.width - currentLine.width) / 2
39
+ : (previousLine.width - currentLine.width) / 4
40
+ : -Infinity, -maxCornerRadius, maxCornerRadius);
41
+ // Top Right Corner
42
+ if (topRightCornerRadius !== 0) {
43
+ instructions.push({
44
+ type: 'L',
45
+ x: xOffset +
46
+ currentLine.width +
47
+ horizontalPadding * 2 +
48
+ topRightCornerRadius,
49
+ y: yOffset,
50
+ });
51
+ // Arc for rounded corner (top right)
52
+ instructions.push({
53
+ type: 'A',
54
+ rx: Math.abs(topRightCornerRadius),
55
+ ry: Math.abs(topRightCornerRadius),
56
+ xAxisRotation: 0,
57
+ largeArcFlag: false,
58
+ sweepFlag: topRightCornerRadius < 0,
59
+ x: xOffset + currentLine.width + horizontalPadding * 2,
60
+ y: yOffset + Math.abs(topRightCornerRadius),
61
+ });
62
+ }
63
+ else {
64
+ instructions.push({
65
+ type: 'L',
66
+ x: xOffset + currentLine.width + horizontalPadding * 2,
67
+ y: yOffset,
68
+ });
69
+ }
70
+ const bottomRightCornerRadius = clamp(nextLine
71
+ ? textAlign === 'right'
72
+ ? 0
73
+ : textAlign === 'left'
74
+ ? (nextLine.width - currentLine.width) / 2
75
+ : (nextLine.width - currentLine.width) / 4
76
+ : -Infinity, -maxCornerRadius, maxCornerRadius);
77
+ // Bottom Right Corner
78
+ if (bottomRightCornerRadius !== 0) {
79
+ instructions.push({
80
+ type: 'L',
81
+ x: xOffset + currentLine.width + horizontalPadding * 2,
82
+ y: yOffset + currentLine.height - Math.abs(bottomRightCornerRadius),
83
+ });
84
+ // Arc for rounded corner (bottom right)
85
+ instructions.push({
86
+ type: 'A',
87
+ rx: Math.abs(bottomRightCornerRadius),
88
+ ry: Math.abs(bottomRightCornerRadius),
89
+ xAxisRotation: 0,
90
+ largeArcFlag: false,
91
+ sweepFlag: bottomRightCornerRadius < 0,
92
+ x: xOffset +
93
+ currentLine.width +
94
+ horizontalPadding * 2 +
95
+ bottomRightCornerRadius,
96
+ y: yOffset + currentLine.height,
97
+ });
98
+ }
99
+ else {
100
+ instructions.push({
101
+ type: 'L',
102
+ x: xOffset + currentLine.width + horizontalPadding * 2,
103
+ y: yOffset + currentLine.height,
104
+ });
105
+ }
106
+ yOffset += currentLine.height;
107
+ }
108
+ for (let i = textMeasurements.length - 1; i >= 0; i--) {
109
+ const cornerRounding = textMeasurements[i];
110
+ const prevCornerRounding = textMeasurements[i + 1];
111
+ const nextCornerRounding = textMeasurements[i - 1];
112
+ let xOffset = 0;
113
+ if (textAlign === 'center') {
114
+ xOffset = (maxWidth - (cornerRounding.width + horizontalPadding * 2)) / 2;
115
+ }
116
+ else if (textAlign === 'right') {
117
+ xOffset = maxWidth - (cornerRounding.width + horizontalPadding * 2);
118
+ }
119
+ const bottomLeftWidthDifference = prevCornerRounding
120
+ ? prevCornerRounding.width - cornerRounding.width
121
+ : -Infinity;
122
+ const maxCornerRadius = clamp(unclampedMaxCornerRadius, 0, cornerRounding.height / 2);
123
+ const bottomLeftCornerRadius = clamp(prevCornerRounding
124
+ ? textAlign === 'left'
125
+ ? 0
126
+ : textAlign === 'right'
127
+ ? bottomLeftWidthDifference / 2
128
+ : bottomLeftWidthDifference / 4
129
+ : -Infinity, -maxCornerRadius, maxCornerRadius);
130
+ // Bottom Left Corner
131
+ if (bottomLeftCornerRadius !== 0) {
132
+ instructions.push({
133
+ type: 'L',
134
+ x: xOffset - bottomLeftCornerRadius,
135
+ y: yOffset,
136
+ });
137
+ // Arc for rounded corner (bottom left)
138
+ instructions.push({
139
+ type: 'A',
140
+ rx: Math.abs(bottomLeftCornerRadius),
141
+ ry: Math.abs(bottomLeftCornerRadius),
142
+ xAxisRotation: 0,
143
+ largeArcFlag: false,
144
+ sweepFlag: bottomLeftCornerRadius < 0,
145
+ x: xOffset,
146
+ y: yOffset - Math.abs(bottomLeftCornerRadius),
147
+ });
148
+ }
149
+ else {
150
+ instructions.push({
151
+ type: 'L',
152
+ x: xOffset,
153
+ y: yOffset,
154
+ });
155
+ }
156
+ const topLeftWidthDifference = nextCornerRounding
157
+ ? nextCornerRounding.width - cornerRounding.width
158
+ : -Infinity;
159
+ const topLeftCornerRadius = clamp(nextCornerRounding
160
+ ? textAlign === 'left'
161
+ ? 0
162
+ : textAlign === 'right'
163
+ ? topLeftWidthDifference / 2
164
+ : topLeftWidthDifference / 4
165
+ : -Infinity, -maxCornerRadius, maxCornerRadius);
166
+ // Top Left Corner
167
+ if (topLeftCornerRadius !== 0) {
168
+ instructions.push({
169
+ type: 'L',
170
+ x: xOffset,
171
+ y: yOffset - cornerRounding.height + Math.abs(topLeftCornerRadius),
172
+ });
173
+ // Arc for rounded corner (top left)
174
+ instructions.push({
175
+ type: 'A',
176
+ rx: Math.abs(topLeftCornerRadius),
177
+ ry: Math.abs(topLeftCornerRadius),
178
+ xAxisRotation: 0,
179
+ largeArcFlag: false,
180
+ sweepFlag: topLeftCornerRadius < 0,
181
+ x: xOffset - topLeftCornerRadius,
182
+ y: yOffset - cornerRounding.height,
183
+ });
184
+ }
185
+ else {
186
+ instructions.push({
187
+ type: 'L',
188
+ x: xOffset,
189
+ y: yOffset - cornerRounding.height,
190
+ });
191
+ }
192
+ yOffset -= cornerRounding.height;
193
+ }
194
+ instructions.push({
195
+ type: 'Z',
196
+ });
197
+ const reduced = (0, paths_1.reduceInstructions)(instructions);
198
+ const boundingBox = paths_1.PathInternals.getBoundingBoxFromInstructions(reduced);
199
+ return {
200
+ d: (0, paths_1.serializeInstructions)(reduced),
201
+ boundingBox,
202
+ instructions: reduced,
203
+ };
204
+ };
205
+ exports.createRoundedTextBox = createRoundedTextBox;
@@ -0,0 +1 @@
1
+ export { createRoundedTextBox, type CreateRoundedTextBoxProps, type CreateRoundedTextBoxResult, type TextAlign, } from './create-rounded-text-box';
@@ -0,0 +1,194 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ entry = __defProp({}, "__esModule", { value: true });
11
+ if (from && typeof from === "object" || typeof from === "function")
12
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
+ get: () => from[key],
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+
29
+ // src/index.ts
30
+ var exports_src = {};
31
+ __export(exports_src, {
32
+ createRoundedTextBox: () => createRoundedTextBox
33
+ });
34
+ module.exports = __toCommonJS(exports_src);
35
+
36
+ // src/create-rounded-text-box.ts
37
+ var import_paths = require("@remotion/paths");
38
+ var clamp = (val, min, max) => {
39
+ return Math.min(Math.max(val, min), max);
40
+ };
41
+ var createRoundedTextBox = ({
42
+ textMeasurements,
43
+ textAlign,
44
+ horizontalPadding,
45
+ borderRadius: unclampedMaxCornerRadius
46
+ }) => {
47
+ const instructions = [];
48
+ let maxWidth = 0;
49
+ for (const cornerRounding of textMeasurements) {
50
+ maxWidth = Math.max(maxWidth, cornerRounding.width + horizontalPadding * 2);
51
+ }
52
+ let yOffset = 0;
53
+ for (let i = 0;i < textMeasurements.length; i++) {
54
+ const previousLine = textMeasurements[i - 1];
55
+ const currentLine = textMeasurements[i];
56
+ const nextLine = textMeasurements[i + 1];
57
+ let xOffset = 0;
58
+ if (textAlign === "center") {
59
+ xOffset = (maxWidth - (currentLine.width + horizontalPadding * 2)) / 2;
60
+ } else if (textAlign === "right") {
61
+ xOffset = maxWidth - (currentLine.width + horizontalPadding * 2);
62
+ }
63
+ const maxCornerRadius = clamp(unclampedMaxCornerRadius, 0, currentLine.height / 2);
64
+ if (i === 0) {
65
+ instructions.push({
66
+ type: "M",
67
+ x: xOffset + maxCornerRadius,
68
+ y: yOffset
69
+ });
70
+ }
71
+ const topRightCornerRadius = clamp(previousLine ? textAlign === "right" ? 0 : textAlign === "left" ? (previousLine.width - currentLine.width) / 2 : (previousLine.width - currentLine.width) / 4 : -Infinity, -maxCornerRadius, maxCornerRadius);
72
+ if (topRightCornerRadius !== 0) {
73
+ instructions.push({
74
+ type: "L",
75
+ x: xOffset + currentLine.width + horizontalPadding * 2 + topRightCornerRadius,
76
+ y: yOffset
77
+ });
78
+ instructions.push({
79
+ type: "A",
80
+ rx: Math.abs(topRightCornerRadius),
81
+ ry: Math.abs(topRightCornerRadius),
82
+ xAxisRotation: 0,
83
+ largeArcFlag: false,
84
+ sweepFlag: topRightCornerRadius < 0,
85
+ x: xOffset + currentLine.width + horizontalPadding * 2,
86
+ y: yOffset + Math.abs(topRightCornerRadius)
87
+ });
88
+ } else {
89
+ instructions.push({
90
+ type: "L",
91
+ x: xOffset + currentLine.width + horizontalPadding * 2,
92
+ y: yOffset
93
+ });
94
+ }
95
+ const bottomRightCornerRadius = clamp(nextLine ? textAlign === "right" ? 0 : textAlign === "left" ? (nextLine.width - currentLine.width) / 2 : (nextLine.width - currentLine.width) / 4 : -Infinity, -maxCornerRadius, maxCornerRadius);
96
+ if (bottomRightCornerRadius !== 0) {
97
+ instructions.push({
98
+ type: "L",
99
+ x: xOffset + currentLine.width + horizontalPadding * 2,
100
+ y: yOffset + currentLine.height - Math.abs(bottomRightCornerRadius)
101
+ });
102
+ instructions.push({
103
+ type: "A",
104
+ rx: Math.abs(bottomRightCornerRadius),
105
+ ry: Math.abs(bottomRightCornerRadius),
106
+ xAxisRotation: 0,
107
+ largeArcFlag: false,
108
+ sweepFlag: bottomRightCornerRadius < 0,
109
+ x: xOffset + currentLine.width + horizontalPadding * 2 + bottomRightCornerRadius,
110
+ y: yOffset + currentLine.height
111
+ });
112
+ } else {
113
+ instructions.push({
114
+ type: "L",
115
+ x: xOffset + currentLine.width + horizontalPadding * 2,
116
+ y: yOffset + currentLine.height
117
+ });
118
+ }
119
+ yOffset += currentLine.height;
120
+ }
121
+ for (let i = textMeasurements.length - 1;i >= 0; i--) {
122
+ const cornerRounding = textMeasurements[i];
123
+ const prevCornerRounding = textMeasurements[i + 1];
124
+ const nextCornerRounding = textMeasurements[i - 1];
125
+ let xOffset = 0;
126
+ if (textAlign === "center") {
127
+ xOffset = (maxWidth - (cornerRounding.width + horizontalPadding * 2)) / 2;
128
+ } else if (textAlign === "right") {
129
+ xOffset = maxWidth - (cornerRounding.width + horizontalPadding * 2);
130
+ }
131
+ const bottomLeftWidthDifference = prevCornerRounding ? prevCornerRounding.width - cornerRounding.width : -Infinity;
132
+ const maxCornerRadius = clamp(unclampedMaxCornerRadius, 0, cornerRounding.height / 2);
133
+ const bottomLeftCornerRadius = clamp(prevCornerRounding ? textAlign === "left" ? 0 : textAlign === "right" ? bottomLeftWidthDifference / 2 : bottomLeftWidthDifference / 4 : -Infinity, -maxCornerRadius, maxCornerRadius);
134
+ if (bottomLeftCornerRadius !== 0) {
135
+ instructions.push({
136
+ type: "L",
137
+ x: xOffset - bottomLeftCornerRadius,
138
+ y: yOffset
139
+ });
140
+ instructions.push({
141
+ type: "A",
142
+ rx: Math.abs(bottomLeftCornerRadius),
143
+ ry: Math.abs(bottomLeftCornerRadius),
144
+ xAxisRotation: 0,
145
+ largeArcFlag: false,
146
+ sweepFlag: bottomLeftCornerRadius < 0,
147
+ x: xOffset,
148
+ y: yOffset - Math.abs(bottomLeftCornerRadius)
149
+ });
150
+ } else {
151
+ instructions.push({
152
+ type: "L",
153
+ x: xOffset,
154
+ y: yOffset
155
+ });
156
+ }
157
+ const topLeftWidthDifference = nextCornerRounding ? nextCornerRounding.width - cornerRounding.width : -Infinity;
158
+ const topLeftCornerRadius = clamp(nextCornerRounding ? textAlign === "left" ? 0 : textAlign === "right" ? topLeftWidthDifference / 2 : topLeftWidthDifference / 4 : -Infinity, -maxCornerRadius, maxCornerRadius);
159
+ if (topLeftCornerRadius !== 0) {
160
+ instructions.push({
161
+ type: "L",
162
+ x: xOffset,
163
+ y: yOffset - cornerRounding.height + Math.abs(topLeftCornerRadius)
164
+ });
165
+ instructions.push({
166
+ type: "A",
167
+ rx: Math.abs(topLeftCornerRadius),
168
+ ry: Math.abs(topLeftCornerRadius),
169
+ xAxisRotation: 0,
170
+ largeArcFlag: false,
171
+ sweepFlag: topLeftCornerRadius < 0,
172
+ x: xOffset - topLeftCornerRadius,
173
+ y: yOffset - cornerRounding.height
174
+ });
175
+ } else {
176
+ instructions.push({
177
+ type: "L",
178
+ x: xOffset,
179
+ y: yOffset - cornerRounding.height
180
+ });
181
+ }
182
+ yOffset -= cornerRounding.height;
183
+ }
184
+ instructions.push({
185
+ type: "Z"
186
+ });
187
+ const reduced = import_paths.reduceInstructions(instructions);
188
+ const boundingBox = import_paths.PathInternals.getBoundingBoxFromInstructions(reduced);
189
+ return {
190
+ d: import_paths.serializeInstructions(reduced),
191
+ boundingBox,
192
+ instructions: reduced
193
+ };
194
+ };
@@ -0,0 +1,10 @@
1
+ import { type Word } from './measure-text';
2
+ export declare const fillTextBox: ({ maxBoxWidth, maxLines, }: {
3
+ maxBoxWidth: number;
4
+ maxLines: number;
5
+ }) => {
6
+ add: ({ text, fontFamily, fontWeight, fontSize, letterSpacing, fontVariantNumeric, validateFontIsLoaded, textTransform, additionalStyles, }: Word) => {
7
+ exceedsBox: boolean;
8
+ newLine: boolean;
9
+ };
10
+ };
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fillTextBox = void 0;
4
+ const measure_text_1 = require("./measure-text");
5
+ const fillTextBox = ({ maxBoxWidth, maxLines, }) => {
6
+ const lines = new Array(maxLines).fill(0).map(() => []);
7
+ return {
8
+ add: ({ text, fontFamily, fontWeight, fontSize, letterSpacing, fontVariantNumeric, validateFontIsLoaded, textTransform, additionalStyles, }) => {
9
+ const lastLineIndex = lines.reduceRight((acc, curr, index) => {
10
+ if (acc === -1 && curr.length > 0) {
11
+ return index;
12
+ }
13
+ return acc;
14
+ }, -1);
15
+ const currentlyAt = lastLineIndex === -1 ? 0 : lastLineIndex;
16
+ const lineToUse = lines[currentlyAt];
17
+ const lineWithWord = [
18
+ ...lineToUse,
19
+ {
20
+ text,
21
+ fontFamily,
22
+ fontWeight,
23
+ fontSize,
24
+ letterSpacing,
25
+ fontVariantNumeric,
26
+ validateFontIsLoaded,
27
+ textTransform,
28
+ additionalStyles,
29
+ },
30
+ ];
31
+ const widths = lineWithWord.map((w) => (0, measure_text_1.measureText)(w).width);
32
+ const lineWidthWithWordAdded = widths.reduce((a, b) => a + b, 0);
33
+ if (Math.ceil(lineWidthWithWordAdded) < maxBoxWidth) {
34
+ lines[currentlyAt].push({
35
+ text: lines[currentlyAt].length === 0 ? text.trimStart() : text,
36
+ fontFamily,
37
+ fontWeight,
38
+ fontSize,
39
+ letterSpacing,
40
+ textTransform,
41
+ fontVariantNumeric,
42
+ });
43
+ return { exceedsBox: false, newLine: false };
44
+ }
45
+ if (currentlyAt === maxLines - 1) {
46
+ return { exceedsBox: true, newLine: false };
47
+ }
48
+ lines[currentlyAt + 1] = [
49
+ {
50
+ text: text.trimStart(),
51
+ fontFamily,
52
+ fontWeight,
53
+ fontSize,
54
+ letterSpacing,
55
+ textTransform,
56
+ fontVariantNumeric,
57
+ },
58
+ ];
59
+ return { exceedsBox: false, newLine: true };
60
+ },
61
+ };
62
+ };
63
+ exports.fillTextBox = fillTextBox;
@@ -0,0 +1,19 @@
1
+ import type { TextTransform } from './measure-text';
2
+ type FitTextOnNLinesProps = {
3
+ text: string;
4
+ maxLines: number;
5
+ maxBoxWidth: number;
6
+ fontFamily: string;
7
+ fontWeight?: number | string;
8
+ letterSpacing?: string;
9
+ fontVariantNumeric?: string;
10
+ validateFontIsLoaded?: boolean;
11
+ textTransform?: TextTransform;
12
+ additionalStyles?: Record<string, string>;
13
+ maxFontSize?: number;
14
+ };
15
+ export declare const fitTextOnNLines: ({ text, maxLines, maxBoxWidth, fontFamily, fontWeight, letterSpacing, fontVariantNumeric, validateFontIsLoaded, textTransform, additionalStyles, maxFontSize, }: FitTextOnNLinesProps) => {
16
+ fontSize: number;
17
+ lines: string[];
18
+ };
19
+ export {};
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fitTextOnNLines = void 0;
4
+ const fill_text_box_1 = require("./fill-text-box");
5
+ const PRECISION = 100;
6
+ const fitTextOnNLines = ({ text, maxLines, maxBoxWidth, fontFamily, fontWeight, letterSpacing, fontVariantNumeric, validateFontIsLoaded, textTransform, additionalStyles, maxFontSize, }) => {
7
+ // Fixed max font size since we are using binary search a
8
+ const minFontSize = 0.1;
9
+ // Binary search to find the optimal font size
10
+ let left = Math.floor(minFontSize * PRECISION);
11
+ let right = Math.floor((maxFontSize !== null && maxFontSize !== void 0 ? maxFontSize : 2000) * PRECISION);
12
+ let optimalFontSize = minFontSize;
13
+ let optimalLines = [];
14
+ while (left <= right) {
15
+ const mid = Math.floor((left + right) / 2);
16
+ const fontSize = mid / PRECISION;
17
+ // Create a text box with current font size
18
+ const textBox = (0, fill_text_box_1.fillTextBox)({
19
+ maxBoxWidth,
20
+ maxLines,
21
+ });
22
+ // Split text into words and try to fit them
23
+ const words = text.split(' ');
24
+ let exceedsBox = false;
25
+ let currentLine = 0;
26
+ const lines = [''];
27
+ for (const word of words) {
28
+ const result = textBox.add({
29
+ text: lines[currentLine].length === 0 ? word : ' ' + word,
30
+ fontFamily,
31
+ fontWeight,
32
+ fontSize,
33
+ letterSpacing,
34
+ fontVariantNumeric,
35
+ validateFontIsLoaded,
36
+ textTransform,
37
+ additionalStyles,
38
+ });
39
+ if (result.exceedsBox) {
40
+ exceedsBox = true;
41
+ break;
42
+ }
43
+ if (result.newLine) {
44
+ lines.push('');
45
+ currentLine++;
46
+ }
47
+ lines[currentLine] += word + ' ';
48
+ }
49
+ // If text fits within the box and number of lines
50
+ if (!exceedsBox && currentLine < maxLines) {
51
+ optimalFontSize = fontSize;
52
+ optimalLines = lines;
53
+ left = mid + 1;
54
+ }
55
+ else {
56
+ right = mid - 1;
57
+ }
58
+ }
59
+ for (let i = 0; i < optimalLines.length; i++) {
60
+ optimalLines[i] = optimalLines[i].trimEnd();
61
+ }
62
+ return {
63
+ fontSize: optimalFontSize,
64
+ lines: optimalLines,
65
+ };
66
+ };
67
+ exports.fitTextOnNLines = fitTextOnNLines;
@@ -0,0 +1,14 @@
1
+ import type { ModifyableCSSProperties, TextTransform } from '../layouts/measure-text';
2
+ export declare const fitText: ({ text, withinWidth, fontFamily, fontVariantNumeric, fontWeight, letterSpacing, validateFontIsLoaded, additionalStyles, textTransform, }: {
3
+ text: string;
4
+ withinWidth: number;
5
+ fontFamily: string;
6
+ fontWeight?: number | string;
7
+ letterSpacing?: string;
8
+ fontVariantNumeric?: string;
9
+ validateFontIsLoaded?: boolean;
10
+ textTransform?: TextTransform;
11
+ additionalStyles?: ModifyableCSSProperties;
12
+ }) => {
13
+ fontSize: number;
14
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fitText = void 0;
4
+ const measure_text_1 = require("../layouts/measure-text");
5
+ const sampleSize = 100;
6
+ /*
7
+ * @description Calculates the font size needed to fit text into a specified width container.
8
+ * @see [Documentation](https://remotion.dev/docs/layout-utils/fit-text)
9
+ */
10
+ const fitText = ({ text, withinWidth, fontFamily, fontVariantNumeric, fontWeight, letterSpacing, validateFontIsLoaded, additionalStyles, textTransform, }) => {
11
+ const estimate = (0, measure_text_1.measureText)({
12
+ text,
13
+ fontFamily,
14
+ fontSize: sampleSize,
15
+ fontWeight,
16
+ fontVariantNumeric,
17
+ letterSpacing,
18
+ validateFontIsLoaded,
19
+ textTransform,
20
+ additionalStyles,
21
+ });
22
+ return { fontSize: (withinWidth / estimate.width) * sampleSize };
23
+ };
24
+ exports.fitText = fitText;
@@ -0,0 +1,23 @@
1
+ export type Dimensions = {
2
+ width: number;
3
+ height: number;
4
+ };
5
+ export type ModifyableCSSProperties<T = Partial<CSSStyleDeclaration>> = {
6
+ [P in keyof T as P extends 'length' ? never : P extends keyof CSSPropertiesOnWord ? never : T[P] extends string | number ? P : never]: T[P];
7
+ };
8
+ export type TextTransform = '-moz-initial' | 'inherit' | 'initial' | 'revert' | 'revert-layer' | 'unset' | 'none' | 'capitalize' | 'full-size-kana' | 'full-width' | 'lowercase' | 'uppercase';
9
+ type CSSPropertiesOnWord = {
10
+ fontFamily: string;
11
+ fontSize: number | string;
12
+ fontWeight?: number | string;
13
+ letterSpacing?: string;
14
+ fontVariantNumeric?: string;
15
+ textTransform?: TextTransform;
16
+ };
17
+ export type Word = {
18
+ text: string;
19
+ validateFontIsLoaded?: boolean;
20
+ additionalStyles?: ModifyableCSSProperties;
21
+ } & CSSPropertiesOnWord;
22
+ export declare const measureText: ({ text, fontFamily, fontSize, fontWeight, letterSpacing, fontVariantNumeric, validateFontIsLoaded, additionalStyles, textTransform, }: Word) => Dimensions;
23
+ export {};
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.measureText = void 0;
4
+ const wordCache = new Map();
5
+ const takeMeasurement = ({ text, fontFamily, fontSize, fontWeight, letterSpacing, fontVariantNumeric, additionalStyles, textTransform, }) => {
6
+ if (typeof document === 'undefined') {
7
+ throw new Error('measureText() can only be called in a browser.');
8
+ }
9
+ const node = document.createElement('span');
10
+ if (fontFamily) {
11
+ node.style.fontFamily = fontFamily;
12
+ }
13
+ node.style.display = 'inline-block';
14
+ node.style.position = 'absolute';
15
+ node.style.top = `-10000px`;
16
+ node.style.whiteSpace = 'pre';
17
+ node.style.fontSize =
18
+ typeof fontSize === 'string' ? fontSize : `${fontSize}px`;
19
+ if (additionalStyles) {
20
+ for (const key of Object.keys(additionalStyles)) {
21
+ node.style[key] = additionalStyles[key];
22
+ }
23
+ }
24
+ if (fontWeight) {
25
+ node.style.fontWeight = fontWeight.toString();
26
+ }
27
+ if (letterSpacing) {
28
+ node.style.letterSpacing = letterSpacing;
29
+ }
30
+ if (fontVariantNumeric) {
31
+ node.style.fontVariantNumeric = fontVariantNumeric;
32
+ }
33
+ if (textTransform) {
34
+ node.style.textTransform = textTransform;
35
+ }
36
+ node.innerText = text;
37
+ document.body.appendChild(node);
38
+ const computedFontFamily = window.getComputedStyle(node).fontFamily;
39
+ const boundingBox = node.getBoundingClientRect();
40
+ document.body.removeChild(node);
41
+ return {
42
+ boundingBox,
43
+ computedFontFamily,
44
+ };
45
+ };
46
+ /*
47
+ * @description Calculates the width and height of specified text to be used for layout calculations. Only works in the browser, not in Node.js or Bun.
48
+ * @see [Documentation](https://remotion.dev/docs/layout-utils/measure-text)
49
+ */
50
+ const measureText = ({ text, fontFamily, fontSize, fontWeight, letterSpacing, fontVariantNumeric, validateFontIsLoaded, additionalStyles, textTransform, }) => {
51
+ const key = `${text}-${fontFamily}-${fontWeight}-${fontSize}-${letterSpacing}-${textTransform}-${JSON.stringify(additionalStyles)}`;
52
+ if (wordCache.has(key)) {
53
+ return wordCache.get(key);
54
+ }
55
+ const { boundingBox, computedFontFamily } = takeMeasurement({
56
+ fontFamily,
57
+ fontSize,
58
+ text,
59
+ fontVariantNumeric,
60
+ fontWeight,
61
+ letterSpacing,
62
+ additionalStyles,
63
+ textTransform,
64
+ });
65
+ if (validateFontIsLoaded && text.trim().length > 0) {
66
+ const { boundingBox: boundingBoxOfFallbackFont, computedFontFamily: computedFallback, } = takeMeasurement({
67
+ fontFamily: null,
68
+ fontSize,
69
+ text,
70
+ fontVariantNumeric,
71
+ fontWeight,
72
+ letterSpacing,
73
+ additionalStyles,
74
+ textTransform,
75
+ });
76
+ const sameAsFallbackFont = boundingBox.height === boundingBoxOfFallbackFont.height &&
77
+ boundingBox.width === boundingBoxOfFallbackFont.width;
78
+ // Ensure there are at least 4 unique characters, with just a few, there is more likely to be a false positive
79
+ if (sameAsFallbackFont &&
80
+ computedFallback !== computedFontFamily &&
81
+ new Set(text).size > 4) {
82
+ const err = [
83
+ `Called measureText() with "fontFamily": ${JSON.stringify(fontFamily)} but it looks like the font is not loaded at the time of calling.`,
84
+ `A measurement with the fallback font ${computedFallback} was taken and had the same dimensions, indicating that the browser used the fallback font.`,
85
+ 'See https://remotion.dev/docs/layout-utils/best-practices for best practices.',
86
+ ];
87
+ throw new Error(err.join('\n'));
88
+ }
89
+ }
90
+ const result = { height: boundingBox.height, width: boundingBox.width };
91
+ wordCache.set(key, result);
92
+ return result;
93
+ };
94
+ exports.measureText = measureText;
@@ -0,0 +1,166 @@
1
+ // src/create-rounded-text-box.ts
2
+ import {
3
+ PathInternals,
4
+ reduceInstructions,
5
+ serializeInstructions
6
+ } from "@remotion/paths";
7
+ var clamp = (val, min, max) => {
8
+ return Math.min(Math.max(val, min), max);
9
+ };
10
+ var createRoundedTextBox = ({
11
+ textMeasurements,
12
+ textAlign,
13
+ horizontalPadding,
14
+ borderRadius: unclampedMaxCornerRadius
15
+ }) => {
16
+ const instructions = [];
17
+ let maxWidth = 0;
18
+ for (const cornerRounding of textMeasurements) {
19
+ maxWidth = Math.max(maxWidth, cornerRounding.width + horizontalPadding * 2);
20
+ }
21
+ let yOffset = 0;
22
+ for (let i = 0;i < textMeasurements.length; i++) {
23
+ const previousLine = textMeasurements[i - 1];
24
+ const currentLine = textMeasurements[i];
25
+ const nextLine = textMeasurements[i + 1];
26
+ let xOffset = 0;
27
+ if (textAlign === "center") {
28
+ xOffset = (maxWidth - (currentLine.width + horizontalPadding * 2)) / 2;
29
+ } else if (textAlign === "right") {
30
+ xOffset = maxWidth - (currentLine.width + horizontalPadding * 2);
31
+ }
32
+ const maxCornerRadius = clamp(unclampedMaxCornerRadius, 0, currentLine.height / 2);
33
+ if (i === 0) {
34
+ instructions.push({
35
+ type: "M",
36
+ x: xOffset + maxCornerRadius,
37
+ y: yOffset
38
+ });
39
+ }
40
+ const topRightCornerRadius = clamp(previousLine ? textAlign === "right" ? 0 : textAlign === "left" ? (previousLine.width - currentLine.width) / 2 : (previousLine.width - currentLine.width) / 4 : -Infinity, -maxCornerRadius, maxCornerRadius);
41
+ if (topRightCornerRadius !== 0) {
42
+ instructions.push({
43
+ type: "L",
44
+ x: xOffset + currentLine.width + horizontalPadding * 2 + topRightCornerRadius,
45
+ y: yOffset
46
+ });
47
+ instructions.push({
48
+ type: "A",
49
+ rx: Math.abs(topRightCornerRadius),
50
+ ry: Math.abs(topRightCornerRadius),
51
+ xAxisRotation: 0,
52
+ largeArcFlag: false,
53
+ sweepFlag: topRightCornerRadius < 0,
54
+ x: xOffset + currentLine.width + horizontalPadding * 2,
55
+ y: yOffset + Math.abs(topRightCornerRadius)
56
+ });
57
+ } else {
58
+ instructions.push({
59
+ type: "L",
60
+ x: xOffset + currentLine.width + horizontalPadding * 2,
61
+ y: yOffset
62
+ });
63
+ }
64
+ const bottomRightCornerRadius = clamp(nextLine ? textAlign === "right" ? 0 : textAlign === "left" ? (nextLine.width - currentLine.width) / 2 : (nextLine.width - currentLine.width) / 4 : -Infinity, -maxCornerRadius, maxCornerRadius);
65
+ if (bottomRightCornerRadius !== 0) {
66
+ instructions.push({
67
+ type: "L",
68
+ x: xOffset + currentLine.width + horizontalPadding * 2,
69
+ y: yOffset + currentLine.height - Math.abs(bottomRightCornerRadius)
70
+ });
71
+ instructions.push({
72
+ type: "A",
73
+ rx: Math.abs(bottomRightCornerRadius),
74
+ ry: Math.abs(bottomRightCornerRadius),
75
+ xAxisRotation: 0,
76
+ largeArcFlag: false,
77
+ sweepFlag: bottomRightCornerRadius < 0,
78
+ x: xOffset + currentLine.width + horizontalPadding * 2 + bottomRightCornerRadius,
79
+ y: yOffset + currentLine.height
80
+ });
81
+ } else {
82
+ instructions.push({
83
+ type: "L",
84
+ x: xOffset + currentLine.width + horizontalPadding * 2,
85
+ y: yOffset + currentLine.height
86
+ });
87
+ }
88
+ yOffset += currentLine.height;
89
+ }
90
+ for (let i = textMeasurements.length - 1;i >= 0; i--) {
91
+ const cornerRounding = textMeasurements[i];
92
+ const prevCornerRounding = textMeasurements[i + 1];
93
+ const nextCornerRounding = textMeasurements[i - 1];
94
+ let xOffset = 0;
95
+ if (textAlign === "center") {
96
+ xOffset = (maxWidth - (cornerRounding.width + horizontalPadding * 2)) / 2;
97
+ } else if (textAlign === "right") {
98
+ xOffset = maxWidth - (cornerRounding.width + horizontalPadding * 2);
99
+ }
100
+ const bottomLeftWidthDifference = prevCornerRounding ? prevCornerRounding.width - cornerRounding.width : -Infinity;
101
+ const maxCornerRadius = clamp(unclampedMaxCornerRadius, 0, cornerRounding.height / 2);
102
+ const bottomLeftCornerRadius = clamp(prevCornerRounding ? textAlign === "left" ? 0 : textAlign === "right" ? bottomLeftWidthDifference / 2 : bottomLeftWidthDifference / 4 : -Infinity, -maxCornerRadius, maxCornerRadius);
103
+ if (bottomLeftCornerRadius !== 0) {
104
+ instructions.push({
105
+ type: "L",
106
+ x: xOffset - bottomLeftCornerRadius,
107
+ y: yOffset
108
+ });
109
+ instructions.push({
110
+ type: "A",
111
+ rx: Math.abs(bottomLeftCornerRadius),
112
+ ry: Math.abs(bottomLeftCornerRadius),
113
+ xAxisRotation: 0,
114
+ largeArcFlag: false,
115
+ sweepFlag: bottomLeftCornerRadius < 0,
116
+ x: xOffset,
117
+ y: yOffset - Math.abs(bottomLeftCornerRadius)
118
+ });
119
+ } else {
120
+ instructions.push({
121
+ type: "L",
122
+ x: xOffset,
123
+ y: yOffset
124
+ });
125
+ }
126
+ const topLeftWidthDifference = nextCornerRounding ? nextCornerRounding.width - cornerRounding.width : -Infinity;
127
+ const topLeftCornerRadius = clamp(nextCornerRounding ? textAlign === "left" ? 0 : textAlign === "right" ? topLeftWidthDifference / 2 : topLeftWidthDifference / 4 : -Infinity, -maxCornerRadius, maxCornerRadius);
128
+ if (topLeftCornerRadius !== 0) {
129
+ instructions.push({
130
+ type: "L",
131
+ x: xOffset,
132
+ y: yOffset - cornerRounding.height + Math.abs(topLeftCornerRadius)
133
+ });
134
+ instructions.push({
135
+ type: "A",
136
+ rx: Math.abs(topLeftCornerRadius),
137
+ ry: Math.abs(topLeftCornerRadius),
138
+ xAxisRotation: 0,
139
+ largeArcFlag: false,
140
+ sweepFlag: topLeftCornerRadius < 0,
141
+ x: xOffset - topLeftCornerRadius,
142
+ y: yOffset - cornerRounding.height
143
+ });
144
+ } else {
145
+ instructions.push({
146
+ type: "L",
147
+ x: xOffset,
148
+ y: yOffset - cornerRounding.height
149
+ });
150
+ }
151
+ yOffset -= cornerRounding.height;
152
+ }
153
+ instructions.push({
154
+ type: "Z"
155
+ });
156
+ const reduced = reduceInstructions(instructions);
157
+ const boundingBox = PathInternals.getBoundingBoxFromInstructions(reduced);
158
+ return {
159
+ d: serializeInstructions(reduced),
160
+ boundingBox,
161
+ instructions: reduced
162
+ };
163
+ };
164
+ export {
165
+ createRoundedTextBox
166
+ };
@@ -0,0 +1,7 @@
1
+ import {remotionFlatConfig} from '@remotion/eslint-config-internal';
2
+
3
+ const config = remotionFlatConfig({react: false});
4
+
5
+ export default {
6
+ ...config,
7
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "repository": {
3
+ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/rounded-text-box"
4
+ },
5
+ "name": "@remotion/rounded-text-box",
6
+ "version": "4.0.361",
7
+ "description": "Create a TikTok-like multiline text box SVG path with rounded corners",
8
+ "main": "dist/cjs/index.js",
9
+ "types": "dist/cjs/index.d.ts",
10
+ "module": "dist/esm/index.mjs",
11
+ "sideEffects": false,
12
+ "scripts": {
13
+ "formatting": "prettier --experimental-cli src --check",
14
+ "lint": "eslint src",
15
+ "make": "tsc -d && bun --env-file=../.env.bundle bundle.ts"
16
+ },
17
+ "exports": {
18
+ "./package.json": "./package.json",
19
+ ".": {
20
+ "types": "./dist/cjs/index.d.ts",
21
+ "require": "./dist/cjs/index.js",
22
+ "module": "./dist/esm/index.mjs",
23
+ "import": "./dist/esm/index.mjs"
24
+ }
25
+ },
26
+ "dependencies": {
27
+ "@remotion/layout-utils": "4.0.361",
28
+ "@remotion/paths": "4.0.361"
29
+ },
30
+ "devDependencies": {
31
+ "@remotion/eslint-config-internal": "4.0.361",
32
+ "eslint": "9.19.0"
33
+ },
34
+ "author": "Jonny Burger <jonny@remotion.dev>",
35
+ "maintainers": [
36
+ "Jonny Burger <jonny@remotion.dev>"
37
+ ],
38
+ "contributors": [],
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://github.com/remotion-dev/remotion/issues"
42
+ },
43
+ "keywords": [
44
+ "remotion",
45
+ "svg"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "homepage": "https://www.remotion.dev/docs/rounded-text-box"
51
+ }