Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit ac1ad18

Browse files
authored
FontVariation.lerp, custom FontVariation constructors, and more documentation (#44996)
This should aid with implementing the framework side of flutter/flutter#105120. This should also address flutter/flutter#28543. This is a reland of #43750 with two changes, one to fix a typo mentioned in #43750 (comment), and one to fix the analysis error found when rolling this to the framework (https://ci.chromium.org/ui/p/flutter/builders/try/Linux%20analyze/62845/overview). The latter change is temporary and can be relaxed when FontVariations is reexported from dart:ui. I plan to do that when fixing flutter/flutter#105120.
1 parent 5a4bbd0 commit ac1ad18

File tree

3 files changed

+257
-22
lines changed

3 files changed

+257
-22
lines changed

lib/ui/text.dart

Lines changed: 219 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,51 @@
33
// found in the LICENSE file.
44
part of dart.ui;
55

6-
/// Whether to slant the glyphs in the font
6+
/// Whether to use the italic type variation of glyphs in the font.
7+
///
8+
/// Some modern fonts allow this to be selected in a more fine-grained manner.
9+
/// See [FontVariation.italic] for details.
10+
///
11+
/// Italic type is distinct from slanted glyphs. To control the slant of a
12+
/// glyph, consider the [FontVariation.slant] font feature.
713
enum FontStyle {
8-
/// Use the upright glyphs
14+
/// Use the upright ("Roman") glyphs.
915
normal,
1016

11-
/// Use glyphs designed for slanting
17+
/// Use glyphs that have a more pronounced angle and typically a cursive style
18+
/// ("italic type").
1219
italic,
1320
}
1421

15-
/// The thickness of the glyphs used to draw the text
22+
/// The thickness of the glyphs used to draw the text.
23+
///
24+
/// Fonts are typically weighted on a 9-point scale, which, for historical
25+
/// reasons, uses the names 100 to 900. In Flutter, these are named `w100` to
26+
/// `w900` and have the following conventional meanings:
27+
///
28+
/// * [w100]: Thin, the thinnest font weight.
29+
///
30+
/// * [w200]: Extra light.
31+
///
32+
/// * [w300]: Light.
33+
///
34+
/// * [w400]: Normal. The constant [FontWeight.normal] is an alias for this value.
35+
///
36+
/// * [w500]: Medium.
37+
///
38+
/// * [w600]: Semi-bold.
39+
///
40+
/// * [w700]: Bold. The constant [FontWeight.bold] is an alias for this value.
41+
///
42+
/// * [w800]: Extra-bold.
43+
///
44+
/// * [w900]: Black, the thickest font weight.
45+
///
46+
/// For example, the font named "Roboto Medium" is typically exposed as a font
47+
/// with the name "Roboto" and the weight [FontWeight.w500].
48+
///
49+
/// Some modern fonts allow the weight to be adjusted in arbitrary increments.
50+
/// See [FontVariation.weight] for details.
1651
class FontWeight {
1752
const FontWeight._(this.index, this.value);
1853

@@ -22,31 +57,31 @@ class FontWeight {
2257
/// The thickness value of this font weight.
2358
final int value;
2459

25-
/// Thin, the least thick
60+
/// Thin, the least thick.
2661
static const FontWeight w100 = FontWeight._(0, 100);
2762

28-
/// Extra-light
63+
/// Extra-light.
2964
static const FontWeight w200 = FontWeight._(1, 200);
3065

31-
/// Light
66+
/// Light.
3267
static const FontWeight w300 = FontWeight._(2, 300);
3368

34-
/// Normal / regular / plain
69+
/// Normal / regular / plain.
3570
static const FontWeight w400 = FontWeight._(3, 400);
3671

37-
/// Medium
72+
/// Medium.
3873
static const FontWeight w500 = FontWeight._(4, 500);
3974

40-
/// Semi-bold
75+
/// Semi-bold.
4176
static const FontWeight w600 = FontWeight._(5, 600);
4277

43-
/// Bold
78+
/// Bold.
4479
static const FontWeight w700 = FontWeight._(6, 700);
4580

46-
/// Extra-bold
81+
/// Extra-bold.
4782
static const FontWeight w800 = FontWeight._(7, 800);
4883

49-
/// Black, the most thick
84+
/// Black, the most thick.
5085
static const FontWeight w900 = FontWeight._(8, 900);
5186

5287
/// The default font weight.
@@ -65,6 +100,9 @@ class FontWeight {
65100
/// Rather than using fractional weights, the interpolation rounds to the
66101
/// nearest weight.
67102
///
103+
/// For a smoother animation of font weight, consider using
104+
/// [FontVariation.weight] if the font in question supports it.
105+
///
68106
/// If both `a` and `b` are null, then this method will return null. Otherwise,
69107
/// any null values for `a` or `b` are interpreted as equivalent to [normal]
70108
/// (also known as [w400]).
@@ -118,6 +156,9 @@ class FontWeight {
118156
/// ** See code in examples/api/lib/ui/text/font_feature.0.dart **
119157
/// {@end-tool}
120158
///
159+
/// Some fonts also support continuous font variations; see the [FontVariation]
160+
/// class.
161+
///
121162
/// See also:
122163
///
123164
/// * <https://en.wikipedia.org/wiki/List_of_typographic_features>,
@@ -938,32 +979,158 @@ class FontFeature {
938979
/// Some fonts are variable fonts that can generate a range of different
939980
/// font faces by altering the values of the font's design axes.
940981
///
941-
/// See https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview
982+
/// For example:
983+
///
984+
/// ```dart
985+
/// TextStyle(fontVariations: <ui.FontVariation>[ui.FontVariation('wght', 800.0)])
986+
/// ```
987+
///
988+
/// Font variations are distinct from font features, as exposed by the
989+
/// [FontFeature] class. Where features can be enabled or disabled in a discrete
990+
/// manner, font variations provide a continuous axis of control.
991+
///
992+
/// See also:
993+
///
994+
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg#registered-axis-tags>,
995+
/// which lists registered axis tags.
942996
///
943-
/// Example:
944-
/// `TextStyle(fontVariations: <FontVariation>[FontVariation('wght', 800.0)])`
997+
/// * <https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview>,
998+
/// an overview of the font variations technology.
945999
class FontVariation {
9461000
/// Creates a [FontVariation] object, which can be added to a [TextStyle] to
9471001
/// change the variable attributes of a font.
9481002
///
9491003
/// `axis` is the four-character tag that identifies the design axis.
950-
/// These tags are specified by font formats such as OpenType.
951-
/// See https://docs.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg
1004+
/// OpenType lists the [currently registered axis
1005+
/// tags](https://docs.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg).
9521006
///
9531007
/// `value` is the value that the axis will be set to. The behavior
9541008
/// depends on how the font implements the axis.
9551009
const FontVariation(
9561010
this.axis,
9571011
this.value,
958-
) : assert(axis.length == 4, 'Axis tag must be exactly four characters long.');
1012+
) : assert(axis.length == 4, 'Axis tag must be exactly four characters long.'),
1013+
assert(value >= -32768.0 && value < 32768.0, 'Value must be representable as a signed 16.16 fixed-point number, i.e. it must be in this range: -32768.0 ≤ value < 32768.0');
1014+
1015+
// Constructors below should be alphabetic by axis tag. This makes it easier
1016+
// to determine when an axis is missing so that we avoid adding duplicates.
1017+
1018+
// Start of axis tag list.
1019+
// ------------------------------------------------------------------------
1020+
1021+
/// Variable font style. (`ital`)
1022+
///
1023+
/// Varies the style of glyphs in the font between normal and italic.
1024+
///
1025+
/// Values must in the range 0.0 (meaning normal, or Roman, as in
1026+
/// [FontStyle.normal]) to 1.0 (meaning fully italic, as in
1027+
/// [FontStyle.italic]).
1028+
///
1029+
/// This is distinct from [FontVariation.slant], which leans the characters
1030+
/// without changing the font style.
1031+
///
1032+
/// See also:
1033+
///
1034+
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_ital>
1035+
const FontVariation.italic(this.value) : assert(value >= 0.0), assert(value <= 1.0), axis = 'ital';
1036+
1037+
/// Optical size optimization. (`opzs`)
1038+
///
1039+
/// Changes the rendering of the font to be optimized for the given text size.
1040+
/// Normally, the optical size of the font will be derived from the font size.
1041+
///
1042+
/// This feature could be used when the text represents a particular physical
1043+
/// font size, for example text in the representation of a hardcopy magazine,
1044+
/// which does not correspond to the actual font size being used to render the
1045+
/// text. By setting the optical size explicitly, font variations that might
1046+
/// be applied as the text is zoomed will be fixed at the size being
1047+
/// represented by the text.
1048+
///
1049+
/// This feature could also be used to smooth animations. If a font varies its
1050+
/// rendering as the font size is adjusted, it may appear to "quiver" (or, one
1051+
/// might even say, "flutter") if the font size is animated. By setting a
1052+
/// fixed optical size, the rendering can be fixed to one particular style as
1053+
/// the text size animates.
1054+
///
1055+
/// Values must be greater than zero, and are interpreted as points. A point
1056+
/// is 1/72 of an inch, or 1.333 logical pixels (96/72).
1057+
///
1058+
/// See also:
1059+
///
1060+
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_opsz>
1061+
const FontVariation.opticalSize(this.value) : assert(value > 0.0), axis = 'opsz';
9591062

960-
/// The tag that identifies the design axis. Must consist of 4 ASCII
961-
/// characters.
1063+
/// Variable font width. (`slnt`)
1064+
///
1065+
/// Varies the slant of glyphs in the font.
1066+
///
1067+
/// Values must be greater than -90.0 and less than +90.0, and represents the
1068+
/// angle in _counter-clockwise_ degrees relative to "normal", at 0.0.
1069+
///
1070+
/// For example, to lean the glyphs forward by 45 degrees, one would use
1071+
/// `FontVariation.slant(-45.0)`.
1072+
///
1073+
/// This is distinct from [FontVariation.italic], in that slant leans the
1074+
/// characters without changing the font style.
1075+
///
1076+
/// See also:
1077+
///
1078+
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_slnt>
1079+
const FontVariation.slant(this.value) : assert(value > -90.0), assert(value < 90.0), axis = 'slnt';
1080+
1081+
/// Variable font width. (`wdth`)
1082+
///
1083+
/// Varies the width of glyphs in the font.
1084+
///
1085+
/// Values must be greater than zero, with no upper limit. 100.0 represents
1086+
/// the "normal" width. Smaller values are "condensed", greater values are
1087+
/// "extended".
1088+
///
1089+
/// See also:
1090+
///
1091+
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_wdth>
1092+
const FontVariation.width(this.value) : assert(value >= 0.0), axis = 'wdth';
1093+
1094+
/// Variable font weight. (`wght`)
1095+
///
1096+
/// Varies the stroke thickness of the font, similar to [FontWeight] but on a
1097+
/// continuous axis.
1098+
///
1099+
/// Values must be in the range 1..1000, and are to be interpreted in a manner
1100+
/// consistent with the values of [FontWeight]. For instance, `400` is the
1101+
/// "normal" weight, and `700` is "bold".
1102+
///
1103+
/// See also:
1104+
///
1105+
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_wght>
1106+
const FontVariation.weight(this.value) : assert(value >= 1), assert(value <= 1000), axis = 'wght';
1107+
1108+
// ------------------------------------------------------------------------
1109+
// End of axis tags list.
1110+
1111+
/// The tag that identifies the design axis.
1112+
///
1113+
/// An axis tag must consist of 4 ASCII characters.
9621114
final String axis;
9631115

9641116
/// The value assigned to this design axis.
9651117
///
9661118
/// The range of usable values depends on the specification of the axis.
1119+
///
1120+
/// While this property is represented as a [double] in this API
1121+
/// ([binary64](https://en.wikipedia.org/wiki/Double-precision_floating-point_format)),
1122+
/// fonts use the fixed-point 16.16 format to represent the value of font
1123+
/// variations. This means that the actual range is -32768.0 to approximately
1124+
/// 32767.999985 and in principle the smallest increment between two values is
1125+
/// approximately 0.000015 (1/65536).
1126+
///
1127+
/// Unfortunately for technical reasons the value is first converted to the
1128+
/// [binary32 floating point
1129+
/// format](https://en.wikipedia.org/wiki/Single-precision_floating-point_format),
1130+
/// which only has 24 bits of precision. This means that for values outside
1131+
/// the range -256.0 to 256.0, the smallest increment is larger than what is
1132+
/// technically supported by OpenType. At the extreme edge of the range, the
1133+
/// smallest increment is only approximately ±0.002.
9671134
final double value;
9681135

9691136
static const int _kEncodedSize = 8;
@@ -989,6 +1156,37 @@ class FontVariation {
9891156
@override
9901157
int get hashCode => Object.hash(axis, value);
9911158

1159+
/// Linearly interpolates between two font variations.
1160+
///
1161+
/// If the two variations have different axis tags, the interpolation switches
1162+
/// abruptly from one to the other at t=0.5. Otherwise, the value is
1163+
/// interpolated (see [lerpDouble].
1164+
///
1165+
/// The value is not clamped to the valid values of the axis tag, but it is
1166+
/// clamped to the valid range of font variations values in general (the range
1167+
/// of signed 16.16 fixed point numbers).
1168+
///
1169+
/// The `t` argument represents position on the timeline, with 0.0 meaning
1170+
/// that the interpolation has not started, returning `a` (or something
1171+
/// equivalent to `a`), 1.0 meaning that the interpolation has finished,
1172+
/// returning `b` (or something equivalent to `b`), and values in between
1173+
/// meaning that the interpolation is at the relevant point on the timeline
1174+
/// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
1175+
/// 1.0, so negative values and values greater than 1.0 are valid (and can
1176+
/// easily be generated by curves such as [Curves.elasticInOut]).
1177+
///
1178+
/// Values for `t` are usually obtained from an [Animation<double>], such as
1179+
/// an [AnimationController].
1180+
static FontVariation? lerp(FontVariation? a, FontVariation? b, double t) {
1181+
if (a?.axis != b?.axis || (a == null && b == null)) {
1182+
return t < 0.5 ? a : b;
1183+
}
1184+
return FontVariation(
1185+
a!.axis,
1186+
clampDouble(lerpDouble(a.value, b!.value, t)!, -32768.0, 32768.0 - 1.0/65536.0),
1187+
);
1188+
}
1189+
9921190
@override
9931191
String toString() => "FontVariation('$axis', $value)";
9941192
}

lib/web_ui/lib/text.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,14 @@ class FontVariation {
181181
const FontVariation(
182182
this.axis,
183183
this.value,
184-
) : assert(axis.length == 4, 'Axis tag must be exactly four characters long.');
184+
) : assert(axis.length == 4, 'Axis tag must be exactly four characters long.'),
185+
assert(value >= -32768.0 && value < 32768.0, 'Value must be representable as a signed 16.16 fixed-point number, i.e. it must be in this range: -32768.0 ≤ value < 32768.0');
186+
187+
const FontVariation.italic(this.value) : assert(value >= 0.0), assert(value <= 1.0), axis = 'ital';
188+
const FontVariation.opticalSize(this.value) : assert(value > 0.0), axis = 'opsz';
189+
const FontVariation.slant(this.value) : assert(value > -90.0), assert(value < 90.0), axis = 'slnt';
190+
const FontVariation.width(this.value) : assert(value >= 0.0), axis = 'wdth';
191+
const FontVariation.weight(this.value) : assert(value >= 1), assert(value <= 1000), axis = 'wght';
185192

186193
final String axis;
187194
final double value;
@@ -199,6 +206,16 @@ class FontVariation {
199206
@override
200207
int get hashCode => Object.hash(axis, value);
201208

209+
static FontVariation? lerp(FontVariation? a, FontVariation? b, double t) {
210+
if (a?.axis != b?.axis || (a == null && b == null)) {
211+
return t < 0.5 ? a : b;
212+
}
213+
return FontVariation(
214+
a!.axis,
215+
clampDouble(lerpDouble(a.value, b!.value, t)!, -32768.0, 32768.0 - 1.0/65536.0),
216+
);
217+
}
218+
202219
@override
203220
String toString() => "FontVariation('$axis', $value)";
204221
}

testing/dart/text_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,26 @@ void testFontVariation() {
285285

286286
expect(wideWidth, greaterThan(baseWidth));
287287
});
288+
289+
test('FontVariation constructors', () async {
290+
expect(const FontVariation.weight(123.0).axis, 'wght');
291+
expect(const FontVariation.weight(123.0).value, 123.0);
292+
expect(const FontVariation.width(123.0).axis, 'wdth');
293+
expect(const FontVariation.width(123.0).value, 123.0);
294+
expect(const FontVariation.slant(45.0).axis, 'slnt');
295+
expect(const FontVariation.slant(45.0).value, 45.0);
296+
expect(const FontVariation.opticalSize(67.0).axis, 'opsz');
297+
expect(const FontVariation.opticalSize(67.0).value, 67.0);
298+
expect(const FontVariation.italic(0.8).axis, 'ital');
299+
expect(const FontVariation.italic(0.8).value, 0.8);
300+
});
301+
302+
test('FontVariation.lerp', () async {
303+
expect(FontVariation.lerp(const FontVariation.weight(100.0), const FontVariation.weight(300.0), 0.5), const FontVariation.weight(200.0));
304+
expect(FontVariation.lerp(const FontVariation.slant(0.0), const FontVariation.slant(-80.0), 0.25), const FontVariation.slant(-20.0));
305+
expect(FontVariation.lerp(const FontVariation.width(90.0), const FontVariation.italic(0.2), 0.1), const FontVariation.width(90.0));
306+
expect(FontVariation.lerp(const FontVariation.width(90.0), const FontVariation.italic(0.2), 0.9), const FontVariation.italic(0.2));
307+
});
288308
}
289309

290310
void testGetWordBoundary() {

0 commit comments

Comments
 (0)