fetta 1.2.0 → 1.3.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.
@@ -63,6 +63,86 @@ var INLINE_ELEMENTS = /* @__PURE__ */ new Set([
63
63
  "u",
64
64
  "var"
65
65
  ]);
66
+ var KERNING_STYLE_PROPS = [
67
+ "font",
68
+ "font-kerning",
69
+ "font-variant-ligatures",
70
+ "font-feature-settings",
71
+ "font-variation-settings",
72
+ "font-optical-sizing",
73
+ "font-size-adjust",
74
+ "font-stretch",
75
+ "font-variant-caps",
76
+ "font-variant-numeric",
77
+ "font-variant-east-asian",
78
+ "font-synthesis",
79
+ "font-synthesis-weight",
80
+ "font-synthesis-style",
81
+ "letter-spacing",
82
+ "word-spacing",
83
+ "text-rendering",
84
+ "text-transform",
85
+ "direction",
86
+ "unicode-bidi"
87
+ ];
88
+ function copyKerningStyles(target, styles) {
89
+ KERNING_STYLE_PROPS.forEach((prop) => {
90
+ const value = styles.getPropertyValue(prop);
91
+ if (value) target.style.setProperty(prop, value);
92
+ });
93
+ }
94
+ function buildCanvasFontString(styles) {
95
+ const fontStyle = styles.fontStyle || "normal";
96
+ const fontWeight = styles.fontWeight || "normal";
97
+ const fontSize = styles.fontSize || "16px";
98
+ const fontFamily = styles.fontFamily || "sans-serif";
99
+ return [fontStyle, fontWeight, fontSize, fontFamily].filter(Boolean).join(" ");
100
+ }
101
+ function applyKerningStylesToCanvas(ctx, styles) {
102
+ ctx.font = buildCanvasFontString(styles);
103
+ const ctxAny = ctx;
104
+ const setIfExists = (prop, value) => {
105
+ if (!value || value === "normal") return;
106
+ if (prop in ctxAny) ctxAny[prop] = value;
107
+ };
108
+ setIfExists("fontKerning", styles.getPropertyValue("font-kerning"));
109
+ setIfExists("fontVariantLigatures", styles.getPropertyValue("font-variant-ligatures"));
110
+ setIfExists("fontFeatureSettings", styles.getPropertyValue("font-feature-settings"));
111
+ setIfExists("fontVariationSettings", styles.getPropertyValue("font-variation-settings"));
112
+ setIfExists("fontOpticalSizing", styles.getPropertyValue("font-optical-sizing"));
113
+ setIfExists("fontSizeAdjust", styles.getPropertyValue("font-size-adjust"));
114
+ setIfExists("fontStretch", styles.getPropertyValue("font-stretch"));
115
+ setIfExists("fontVariantCaps", styles.getPropertyValue("font-variant-caps"));
116
+ setIfExists("fontVariantNumeric", styles.getPropertyValue("font-variant-numeric"));
117
+ setIfExists("fontVariantEastAsian", styles.getPropertyValue("font-variant-east-asian"));
118
+ setIfExists("fontSynthesis", styles.getPropertyValue("font-synthesis"));
119
+ setIfExists("fontSynthesisWeight", styles.getPropertyValue("font-synthesis-weight"));
120
+ setIfExists("fontSynthesisStyle", styles.getPropertyValue("font-synthesis-style"));
121
+ setIfExists("letterSpacing", styles.getPropertyValue("letter-spacing"));
122
+ setIfExists("wordSpacing", styles.getPropertyValue("word-spacing"));
123
+ setIfExists("textRendering", styles.getPropertyValue("text-rendering"));
124
+ setIfExists("direction", styles.getPropertyValue("direction"));
125
+ }
126
+ function buildKerningStyleKey(styles) {
127
+ return KERNING_STYLE_PROPS.map((prop) => styles.getPropertyValue(prop)).join("|");
128
+ }
129
+ function shouldUseDomKerning(styles) {
130
+ const textTransform = styles.getPropertyValue("text-transform");
131
+ if (textTransform && textTransform !== "none") return true;
132
+ const fontVariant = styles.getPropertyValue("font-variant");
133
+ if (fontVariant && fontVariant !== "normal") return true;
134
+ const fontStretch = styles.getPropertyValue("font-stretch");
135
+ if (fontStretch && fontStretch !== "normal" && fontStretch !== "100%") return true;
136
+ const fontFeatureSettings = styles.getPropertyValue("font-feature-settings");
137
+ if (fontFeatureSettings && fontFeatureSettings !== "normal") return true;
138
+ const fontVariationSettings = styles.getPropertyValue("font-variation-settings");
139
+ if (fontVariationSettings && fontVariationSettings !== "normal") return true;
140
+ const fontOpticalSizing = styles.getPropertyValue("font-optical-sizing");
141
+ if (fontOpticalSizing && fontOpticalSizing !== "auto") return true;
142
+ const fontSizeAdjust = styles.getPropertyValue("font-size-adjust");
143
+ if (fontSizeAdjust && fontSizeAdjust !== "none") return true;
144
+ return false;
145
+ }
66
146
  var isSafariBrowser = null;
67
147
  function isSafari() {
68
148
  if (isSafariBrowser !== null) return isSafariBrowser;
@@ -70,21 +150,14 @@ function isSafari() {
70
150
  isSafariBrowser = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
71
151
  return isSafariBrowser;
72
152
  }
73
- function measureKerningCanvas(element, chars) {
153
+ function measureKerningCanvas(styleSource, chars, styles) {
74
154
  const kerningMap = /* @__PURE__ */ new Map();
75
155
  if (chars.length < 2) return kerningMap;
76
156
  const canvas = document.createElement("canvas");
77
157
  const ctx = canvas.getContext("2d");
78
158
  if (!ctx) return kerningMap;
79
- const styles = getComputedStyle(element);
80
- ctx.font = `${styles.fontStyle} ${styles.fontWeight} ${styles.fontSize} ${styles.fontFamily}`;
81
- if (styles.letterSpacing && styles.letterSpacing !== "normal") {
82
- ctx.letterSpacing = styles.letterSpacing;
83
- }
84
- if (styles.wordSpacing && styles.wordSpacing !== "normal") {
85
- ctx.wordSpacing = styles.wordSpacing;
86
- }
87
- if ("fontVariantLigatures" in ctx) ctx.fontVariantLigatures = "none";
159
+ const computedStyles = styles != null ? styles : getComputedStyle(styleSource);
160
+ applyKerningStylesToCanvas(ctx, computedStyles);
88
161
  const charWidths = /* @__PURE__ */ new Map();
89
162
  for (const char of new Set(chars)) {
90
163
  charWidths.set(char, ctx.measureText(char).width);
@@ -100,7 +173,7 @@ function measureKerningCanvas(element, chars) {
100
173
  }
101
174
  return kerningMap;
102
175
  }
103
- function measureKerningDOM(element, chars) {
176
+ function measureKerningDOM(container, styleSource, chars, styles) {
104
177
  const kerningMap = /* @__PURE__ */ new Map();
105
178
  if (chars.length < 2) return kerningMap;
106
179
  const measurer = document.createElement("span");
@@ -109,21 +182,17 @@ function measureKerningDOM(element, chars) {
109
182
  visibility: hidden;
110
183
  white-space: pre;
111
184
  `;
112
- const styles = getComputedStyle(element);
113
- measurer.style.font = styles.font;
114
- measurer.style.letterSpacing = styles.letterSpacing;
115
- measurer.style.wordSpacing = styles.wordSpacing;
116
- measurer.style.fontKerning = styles.fontKerning;
117
- measurer.style.fontVariantLigatures = "none";
118
- const webkitSmoothing = styles.webkitFontSmoothing || styles["-webkit-font-smoothing"];
119
- const mozSmoothing = styles.MozOsxFontSmoothing || styles["-moz-osx-font-smoothing"];
185
+ const computedStyles = styles != null ? styles : getComputedStyle(styleSource);
186
+ copyKerningStyles(measurer, computedStyles);
187
+ const webkitSmoothing = computedStyles.webkitFontSmoothing || computedStyles["-webkit-font-smoothing"];
188
+ const mozSmoothing = computedStyles.MozOsxFontSmoothing || computedStyles["-moz-osx-font-smoothing"];
120
189
  if (webkitSmoothing) {
121
190
  measurer.style.webkitFontSmoothing = webkitSmoothing;
122
191
  }
123
192
  if (mozSmoothing) {
124
193
  measurer.style.MozOsxFontSmoothing = mozSmoothing;
125
194
  }
126
- element.appendChild(measurer);
195
+ container.appendChild(measurer);
127
196
  const charWidths = /* @__PURE__ */ new Map();
128
197
  for (const char of new Set(chars)) {
129
198
  measurer.textContent = char;
@@ -139,12 +208,13 @@ function measureKerningDOM(element, chars) {
139
208
  kerningMap.set(i + 1, kerning);
140
209
  }
141
210
  }
142
- element.removeChild(measurer);
211
+ container.removeChild(measurer);
143
212
  return kerningMap;
144
213
  }
145
- function measureKerning(element, chars) {
214
+ function measureKerning(container, styleSource, chars, styles) {
146
215
  if (chars.length < 2) return /* @__PURE__ */ new Map();
147
- return isSafari() ? measureKerningDOM(element, chars) : measureKerningCanvas(element, chars);
216
+ const computedStyles = styles != null ? styles : getComputedStyle(styleSource);
217
+ return isSafari() || shouldUseDomKerning(computedStyles) ? measureKerningDOM(container, styleSource, chars, computedStyles) : measureKerningCanvas(styleSource, chars, computedStyles);
148
218
  }
149
219
  var srOnlyStylesInjected = false;
150
220
  function injectSrOnlyStyles() {
@@ -407,7 +477,6 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
407
477
  }
408
478
  charGroups.forEach((group) => {
409
479
  group.chars.forEach((measuredChar) => {
410
- measuredWord.chars.indexOf(measuredChar);
411
480
  const charSpan = createSpan(charClass, globalCharIndex, "inline-block", {
412
481
  propIndex: options == null ? void 0 : options.propIndex,
413
482
  propName: "char"
@@ -514,13 +583,37 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
514
583
  for (const wordSpan of allWords) {
515
584
  const wordChars = Array.from(wordSpan.querySelectorAll(`.${charClass}`));
516
585
  if (wordChars.length < 2) continue;
517
- const charStrings = wordChars.map((c) => c.textContent || "");
518
- const kerningMap = measureKerning(element, charStrings);
519
- for (const [charIndex, kerning] of kerningMap) {
520
- const charSpan = wordChars[charIndex];
521
- if (charSpan && Math.abs(kerning) < 20) {
522
- const targetElement = (options == null ? void 0 : options.mask) === "chars" && charSpan.parentElement ? charSpan.parentElement : charSpan;
523
- targetElement.style.marginLeft = `${kerning}px`;
586
+ const styleGroups = [];
587
+ const firstCharStyles = getComputedStyle(wordChars[0]);
588
+ let currentKey = buildKerningStyleKey(firstCharStyles);
589
+ let currentGroup = {
590
+ chars: [wordChars[0]],
591
+ styleSource: wordChars[0],
592
+ styles: firstCharStyles
593
+ };
594
+ for (let i2 = 1; i2 < wordChars.length; i2++) {
595
+ const char = wordChars[i2];
596
+ const charStyles = getComputedStyle(char);
597
+ const key = buildKerningStyleKey(charStyles);
598
+ if (key === currentKey) {
599
+ currentGroup.chars.push(char);
600
+ } else {
601
+ styleGroups.push(currentGroup);
602
+ currentKey = key;
603
+ currentGroup = { chars: [char], styleSource: char, styles: charStyles };
604
+ }
605
+ }
606
+ styleGroups.push(currentGroup);
607
+ for (const group of styleGroups) {
608
+ if (group.chars.length < 2) continue;
609
+ const charStrings = group.chars.map((c) => c.textContent || "");
610
+ const kerningMap = measureKerning(element, group.styleSource, charStrings, group.styles);
611
+ for (const [charIndex, kerning] of kerningMap) {
612
+ const charSpan = group.chars[charIndex];
613
+ if (charSpan && Math.abs(kerning) < 20) {
614
+ const targetElement = (options == null ? void 0 : options.mask) === "chars" && charSpan.parentElement ? charSpan.parentElement : charSpan;
615
+ targetElement.style.marginLeft = `${kerning}px`;
616
+ }
524
617
  }
525
618
  }
526
619
  }
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { splitText } from './chunk-PRR25BMJ.js';
1
+ export { splitText } from './chunk-Q2D5AQBW.js';
package/dist/react.d.ts CHANGED
@@ -43,6 +43,12 @@ type CallbackReturn = void | {
43
43
  }> | Promise<unknown>;
44
44
  interface SplitTextProps {
45
45
  children: ReactElement;
46
+ /** The wrapper element type. Default: "div" */
47
+ as?: keyof React.JSX.IntrinsicElements;
48
+ /** Class name for the wrapper element */
49
+ className?: string;
50
+ /** Additional styles for the wrapper element (merged with internal styles) */
51
+ style?: React.CSSProperties;
46
52
  /**
47
53
  * Called after text is split.
48
54
  * Return an animation or promise to enable revert (requires revertOnComplete).
@@ -115,6 +121,6 @@ interface SplitTextProps {
115
121
  * </SplitText>
116
122
  * ```
117
123
  */
118
- declare const SplitText: react.ForwardRefExoticComponent<SplitTextProps & react.RefAttributes<HTMLDivElement>>;
124
+ declare const SplitText: react.ForwardRefExoticComponent<SplitTextProps & react.RefAttributes<HTMLElement>>;
119
125
 
120
126
  export { SplitText, type SplitTextElements };
package/dist/react.js CHANGED
@@ -1,10 +1,13 @@
1
- import { splitText, __spreadProps, __spreadValues, normalizeToPromise } from './chunk-PRR25BMJ.js';
1
+ import { splitText, __spreadProps, __spreadValues, normalizeToPromise } from './chunk-Q2D5AQBW.js';
2
2
  import { forwardRef, useRef, useCallback, useState, useLayoutEffect, useEffect, isValidElement, cloneElement } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
5
5
  var SplitText = forwardRef(
6
6
  function SplitText2({
7
7
  children,
8
+ as: Component = "div",
9
+ className,
10
+ style: userStyle,
8
11
  onSplit,
9
12
  onResize,
10
13
  options,
@@ -167,11 +170,13 @@ var SplitText = forwardRef(
167
170
  const clonedChild = cloneElement(children, {
168
171
  ref: childRefCallback
169
172
  });
173
+ const Wrapper = Component;
170
174
  return /* @__PURE__ */ jsx(
171
- "div",
175
+ Wrapper,
172
176
  {
173
177
  ref: mergedRef,
174
- style: { visibility: "hidden", position: "relative" },
178
+ className,
179
+ style: __spreadValues({ visibility: "hidden", position: "relative" }, userStyle),
175
180
  children: clonedChild
176
181
  }
177
182
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetta",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Text splitting library with kerning compensation for animations",
5
5
  "type": "module",
6
6
  "sideEffects": false,