Skip to content

Commit

Permalink
FontVariation.lerp, custom FontVariation constructors, and more docum…
Browse files Browse the repository at this point in the history
…entation (flutter#43750)

This should aid with implementing the framework side of flutter/flutter#105120 This should also address flutter/flutter#28543.
  • Loading branch information
Hixie authored Aug 23, 2023
1 parent 6477233 commit f48bdba
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 22 deletions.
240 changes: 219 additions & 21 deletions lib/ui/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,51 @@
// found in the LICENSE file.
part of dart.ui;

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

/// Use glyphs designed for slanting
/// Use glyphs that have a more pronounced angle and typically a cursive style
/// ("italic type").
italic,
}

/// The thickness of the glyphs used to draw the text
/// The thickness of the glyphs used to draw the text.
///
/// Fonts are typically weighted on a 9-point scale, which, for historical
/// reasons, uses the names 100 to 900. In Flutter, these are named `w100` to
/// `w900` and have the following conventional meanings:
///
/// * [w100]: Thin, the thinnest font weight.
///
/// * [w200]: Extra light.
///
/// * [w300]: Light.
///
/// * [w400]: Normal. The constant [FontWeight.normal] is an alias for this value.
///
/// * [w500]: Medium.
///
/// * [w600]: Semi-bold.
///
/// * [w700]: Bold. The constant [FontWeight.bold] is an alias for this value.
///
/// * [w800]: Extra-bold.
///
/// * [w900]: Black, the thickest font meight.
///
/// For example, the font named "Roboto Medium" is typically exposed as a font
/// with the name "Roboto" and the weight [FontWeight.w500].
///
/// Some modern fonts allow the weight to be adjusted in arbitrary increments.
/// See [FontVariation.weight] for details.
class FontWeight {
const FontWeight._(this.index, this.value);

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

/// Thin, the least thick
/// Thin, the least thick.
static const FontWeight w100 = FontWeight._(0, 100);

/// Extra-light
/// Extra-light.
static const FontWeight w200 = FontWeight._(1, 200);

/// Light
/// Light.
static const FontWeight w300 = FontWeight._(2, 300);

/// Normal / regular / plain
/// Normal / regular / plain.
static const FontWeight w400 = FontWeight._(3, 400);

/// Medium
/// Medium.
static const FontWeight w500 = FontWeight._(4, 500);

/// Semi-bold
/// Semi-bold.
static const FontWeight w600 = FontWeight._(5, 600);

/// Bold
/// Bold.
static const FontWeight w700 = FontWeight._(6, 700);

/// Extra-bold
/// Extra-bold.
static const FontWeight w800 = FontWeight._(7, 800);

/// Black, the most thick
/// Black, the most thick.
static const FontWeight w900 = FontWeight._(8, 900);

/// The default font weight.
Expand All @@ -65,6 +100,9 @@ class FontWeight {
/// Rather than using fractional weights, the interpolation rounds to the
/// nearest weight.
///
/// For a smoother animation of font weight, consider using
/// [FontVariation.weight] if the font in question supports it.
///
/// If both `a` and `b` are null, then this method will return null. Otherwise,
/// any null values for `a` or `b` are interpreted as equivalent to [normal]
/// (also known as [w400]).
Expand Down Expand Up @@ -118,6 +156,9 @@ class FontWeight {
/// ** See code in examples/api/lib/ui/text/font_feature.0.dart **
/// {@end-tool}
///
/// Some fonts also support continuous font variations; see the [FontVariation]
/// class.
///
/// See also:
///
/// * <https://en.wikipedia.org/wiki/List_of_typographic_features>,
Expand Down Expand Up @@ -938,32 +979,158 @@ class FontFeature {
/// Some fonts are variable fonts that can generate a range of different
/// font faces by altering the values of the font's design axes.
///
/// See https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview
/// For example:
///
/// ```dart
/// TextStyle(fontVariations: <FontVariation>[FontVariation('wght', 800.0)])
/// ```
///
/// Font variations are distinct from font features, as exposed by the
/// [FontFeature] class. Where features can be enabled or disabled in a discrete
/// manner, font variations provide a continuous axis of control.
///
/// See also:
///
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg#registered-axis-tags>,
/// which lists registered axis tags.
///
/// Example:
/// `TextStyle(fontVariations: <FontVariation>[FontVariation('wght', 800.0)])`
/// * <https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview>,
/// an overview of the font variations technology.
class FontVariation {
/// Creates a [FontVariation] object, which can be added to a [TextStyle] to
/// change the variable attributes of a font.
///
/// `axis` is the four-character tag that identifies the design axis.
/// These tags are specified by font formats such as OpenType.
/// See https://docs.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg
/// OpenType lists the [currently registered axis
/// tags](https://docs.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg).
///
/// `value` is the value that the axis will be set to. The behavior
/// depends on how the font implements the axis.
const FontVariation(
this.axis,
this.value,
) : assert(axis.length == 4, 'Axis tag must be exactly four characters long.');
) : assert(axis.length == 4, 'Axis tag must be exactly four characters long.'),
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');

// Constructors below should be alphabetic by axis tag. This makes it easier
// to determine when an axis is missing so that we avoid adding duplicates.

// Start of axis tag list.
// ------------------------------------------------------------------------

/// Variable font style. (`ital`)
///
/// Varies the style of glyphs in the font between normal and italic.
///
/// Values must in the range 0.0 (meaning normal, or Roman, as in
/// [FontStyle.normal]) to 1.0 (meaning fully italic, as in
/// [FontStyle.italic]).
///
/// This is distinct from [FontVariation.slant], which leans the characters
/// without changing the font style.
///
/// See also:
///
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_ital>
const FontVariation.italic(this.value) : assert(value >= 0.0), assert(value <= 1.0), axis = 'ital';

/// Optical size optimization. (`opzs`)
///
/// Changes the rendering of the font to be optimized for the given text size.
/// Normally, the optical size of the font will be derived from the font size.
///
/// This feature could be used when the text represents a particular physical
/// font size, for example text in the representation of a hardcopy magazine,
/// which does not correspond to the actual font size being used to render the
/// text. By setting the optical size explicitly, font variations that might
/// be applied as the text is zoomed will be fixed at the size being
/// represented by the text.
///
/// This feature could also be used to smooth animations. If a font varies its
/// rendering as the font size is adjusted, it may appear to "quiver" (or, one
/// might even say, "flutter") if the font size is animated. By setting a
/// fixed optical size, the rendering can be fixed to one particular style as
/// the text size animates.
///
/// Values must be greater than zero, and are interpreted as points. A point
/// is 1/72 of an inch, or 1.333 logical pixels (96/72).
///
/// See also:
///
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_opsz>
const FontVariation.opticalSize(this.value) : assert(value > 0.0), axis = 'opsz';

/// The tag that identifies the design axis. Must consist of 4 ASCII
/// characters.
/// Variable font width. (`slnt`)
///
/// Varies the slant of glyphs in the font.
///
/// Values must be greater than -90.0 and less than +90.0, and represents the
/// angle in _counter-clockwise_ degrees relative to "normal", at 0.0.
///
/// For example, to lean the glyphs forward by 45 degrees, one would use
/// `FontVariation.slant(-45.0)`.
///
/// This is distinct from [FontVariation.italic], in that slant leans the
/// characters without changing the font style.
///
/// See also:
///
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_slnt>
const FontVariation.slant(this.value) : assert(value > -90.0), assert(value < 90.0), axis = 'slnt';

/// Variable font width. (`wdth`)
///
/// Varies the width of glyphs in the font.
///
/// Values must be greater than zero, with no upper limit. 100.0 represents
/// the "normal" width. Smaller values are "condensed", greater values are
/// "extended".
///
/// See also:
///
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_wdth>
const FontVariation.width(this.value) : assert(value >= 0.0), axis = 'wdth';

/// Variable font weight. (`wght`)
///
/// Varies the stroke thickness of the font, similar to [FontWeight] but on a
/// continuous axis.
///
/// Values must be in the range 1..1000, and are to be interpreted in a manner
/// consistent with the values of [FontWeight]. For instance, `400` is the
/// "normal" weight, and `700` is "bold".
///
/// See also:
///
/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_wght>
const FontVariation.weight(this.value) : assert(value >= 1), assert(value <= 1000), axis = 'wght';

// ------------------------------------------------------------------------
// End of axis tags list.

/// The tag that identifies the design axis.
///
/// An axis tag must consist of 4 ASCII characters.
final String axis;

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

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

/// Linearly interpolates between two font variations.
///
/// If the two variations have different axis tags, the interpolation switches
/// abruptly from one to the other at t=0.5. Otherwise, the value is
/// interpolated (see [lerpDouble].
///
/// The value is not clamped to the valid values of the axis tag, but it is
/// clamped to the valid range of font variations values in general (the range
/// of signed 16.16 fixed point numbers).
///
/// The `t` argument represents position on the timeline, with 0.0 meaning
/// that the interpolation has not started, returning `a` (or something
/// equivalent to `a`), 1.0 meaning that the interpolation has finished,
/// returning `b` (or something equivalent to `b`), and values in between
/// meaning that the interpolation is at the relevant point on the timeline
/// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
/// 1.0, so negative values and values greater than 1.0 are valid (and can
/// easily be generated by curves such as [Curves.elasticInOut]).
///
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static FontVariation? lerp(FontVariation? a, FontVariation? b, double t) {
if (a?.axis != b?.axis || (a == null && b == null)) {
return t < 0.5 ? a : b;
}
return FontVariation(
a!.axis,
clampDouble(lerpDouble(a.value, b!.value, t)!, -32768.0, 32768.0 - 1.0/65536.0),
);
}

@override
String toString() => "FontVariation('$axis', $value)";
}
Expand Down
19 changes: 18 additions & 1 deletion lib/web_ui/lib/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,14 @@ class FontVariation {
const FontVariation(
this.axis,
this.value,
) : assert(axis.length == 4, 'Axis tag must be exactly four characters long.');
) : assert(axis.length == 4, 'Axis tag must be exactly four characters long.'),
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');

const FontVariation.italic(this.value) : assert(value >= 0.0), assert(value <= 1.0), axis = 'ital';
const FontVariation.opticalSize(this.value) : assert(value > 0.0), axis = 'opsz';
const FontVariation.slant(this.value) : assert(value > -90.0), assert(value < 90.0), axis = 'slnt';
const FontVariation.width(this.value) : assert(value >= 0.0), axis = 'wdth';
const FontVariation.weight(this.value) : assert(value >= 1), assert(value <= 1000), axis = 'wght';

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

static FontVariation? lerp(FontVariation? a, FontVariation? b, double t) {
if (a?.axis != b?.axis || (a == null && b == null)) {
return t < 0.5 ? a : b;
}
return FontVariation(
a!.axis,
clampDouble(lerpDouble(a.value, b!.value, t)!, -32768.0, 32768.0 - 1.0/65536.0),
);
}

@override
String toString() => "FontVariation('$axis', $value)";
}
Expand Down
20 changes: 20 additions & 0 deletions testing/dart/text_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,26 @@ void testFontVariation() {

expect(wideWidth, greaterThan(baseWidth));
});

test('FontVariation constructors', () async {
expect(const FontVariation.weight(123.0).axis, 'wght');
expect(const FontVariation.weight(123.0).value, 123.0);
expect(const FontVariation.width(123.0).axis, 'wdth');
expect(const FontVariation.width(123.0).value, 123.0);
expect(const FontVariation.slant(45.0).axis, 'slnt');
expect(const FontVariation.slant(45.0).value, 45.0);
expect(const FontVariation.opticalSize(67.0).axis, 'opsz');
expect(const FontVariation.opticalSize(67.0).value, 67.0);
expect(const FontVariation.italic(0.8).axis, 'ital');
expect(const FontVariation.italic(0.8).value, 0.8);
});

test('FontVariation.lerp', () async {
expect(FontVariation.lerp(const FontVariation.weight(100.0), const FontVariation.weight(300.0), 0.5), const FontVariation.weight(200.0));
expect(FontVariation.lerp(const FontVariation.slant(0.0), const FontVariation.slant(-80.0), 0.25), const FontVariation.slant(-20.0));
expect(FontVariation.lerp(const FontVariation.width(90.0), const FontVariation.italic(0.2), 0.1), const FontVariation.width(90.0));
expect(FontVariation.lerp(const FontVariation.width(90.0), const FontVariation.italic(0.2), 0.9), const FontVariation.italic(0.2));
});
}

void testGetWordBoundary() {
Expand Down

0 comments on commit f48bdba

Please sign in to comment.