flutter_logo.dart 15.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;
6
import 'dart:ui' as ui show Gradient, TextBox, lerpDouble;
7

8
import 'package:flutter/foundation.dart';
9

10
import 'alignment.dart';
11
import 'basic_types.dart';
12
import 'box_fit.dart';
13
import 'colors.dart';
14
import 'decoration.dart';
15
import 'edge_insets.dart';
16
import 'image_provider.dart';
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
import 'text_painter.dart';
import 'text_span.dart';
import 'text_style.dart';

/// Possible ways to draw Flutter's logo.
enum FlutterLogoStyle {
  /// Show only Flutter's logo, not the "Flutter" label.
  ///
  /// This is the default behavior for [FlutterLogoDecoration] objects.
  markOnly,

  /// Show Flutter's logo on the left, and the "Flutter" label to its right.
  horizontal,

  /// Show Flutter's logo above the "Flutter" label.
  stacked,
}

/// An immutable description of how to paint Flutter's logo.
class FlutterLogoDecoration extends Decoration {
  /// Creates a decoration that knows how to paint Flutter's logo.
  ///
39 40
  /// The [style] controls whether and where to draw the "Flutter" label. If one
  /// is shown, the [textColor] controls the color of the label.
41
  ///
42
  /// The [textColor], [style], and [margin] arguments must not be null.
43
  const FlutterLogoDecoration({
44
    this.textColor = const Color(0xFF757575),
45 46
    this.style = FlutterLogoStyle.markOnly,
    this.margin = EdgeInsets.zero,
47
  }) : _position = identical(style, FlutterLogoStyle.markOnly) ? 0.0 : identical(style, FlutterLogoStyle.horizontal) ? 1.0 : -1.0,
48 49
       _opacity = 1.0;

50
  const FlutterLogoDecoration._(this.textColor, this.style, this.margin, this._position, this._opacity);
51

52
  /// The color used to paint the "Flutter" text on the logo, if [style] is
53 54 55 56
  /// [FlutterLogoStyle.horizontal] or [FlutterLogoStyle.stacked].
  ///
  /// If possible, the default (a medium grey) should be used against a white
  /// background.
57 58
  final Color textColor;

59 60 61 62 63 64
  /// Whether and where to draw the "Flutter" text. By default, only the logo
  /// itself is drawn.
  // This property isn't actually used when painting. It's only really used to
  // set the internal _position property.
  final FlutterLogoStyle style;

65 66 67
  /// How far to inset the logo from the edge of the container.
  final EdgeInsets margin;

68 69 70 71 72 73 74 75
  // The following are set when lerping, to represent states that can't be
  // represented by the constructor.
  final double _position; // -1.0 for stacked, 1.0 for horizontal, 0.0 for no logo
  final double _opacity; // 0.0 .. 1.0

  bool get _inTransition => _opacity != 1.0 || (_position != -1.0 && _position != 0.0 && _position != 1.0);

  @override
76
  bool debugAssertIsValid() {
77
    assert(
78
      _position.isFinite
79
        && _opacity >= 0.0
80 81
        && _opacity <= 1.0,
    );
82 83 84 85 86 87 88 89 90 91
    return true;
  }

  @override
  bool get isComplex => !_inTransition;

  /// Linearly interpolate between two Flutter logo descriptions.
  ///
  /// Interpolates both the color and the style in a continuous fashion.
  ///
92 93
  /// If both values are null, this returns null. Otherwise, it returns a
  /// non-null value. If one of the values is null, then the result is obtained
94
  /// by scaling the other value's opacity and [margin].
95
  ///
96
  /// {@macro dart.ui.shadow.lerp}
97 98 99 100
  ///
  /// See also:
  ///
  ///  * [Decoration.lerp], which interpolates between arbitrary decorations.
101
  static FlutterLogoDecoration? lerp(FlutterLogoDecoration? a, FlutterLogoDecoration? b, double t) {
102 103
    assert(a == null || a.debugAssertIsValid());
    assert(b == null || b.debugAssertIsValid());
104 105
    if (identical(a, b)) {
      return a;
106
    }
107
    if (a == null) {
108
      return FlutterLogoDecoration._(
109
        b!.textColor,
110
        b.style,
111
        b.margin * t,
112
        b._position,
113
        b._opacity * clampDouble(t, 0.0, 1.0),
114 115 116
      );
    }
    if (b == null) {
117
      return FlutterLogoDecoration._(
118
        a.textColor,
119
        a.style,
120
        a.margin * t,
121
        a._position,
122
        a._opacity * clampDouble(1.0 - t, 0.0, 1.0),
123 124
      );
    }
125
    if (t == 0.0) {
126
      return a;
127 128
    }
    if (t == 1.0) {
129
      return b;
130
    }
131
    return FlutterLogoDecoration._(
132
      Color.lerp(a.textColor, b.textColor, t)!,
133
      t < 0.5 ? a.style : b.style,
134
      EdgeInsets.lerp(a.margin, b.margin, t)!,
135
      a._position + (b._position - a._position) * t,
136
      clampDouble(a._opacity + (b._opacity - a._opacity) * t, 0.0, 1.0),
137 138 139 140
    );
  }

  @override
141
  FlutterLogoDecoration? lerpFrom(Decoration? a, double t) {
142
    assert(debugAssertIsValid());
143 144
    if (a == null || a is FlutterLogoDecoration) {
      assert(a == null || a.debugAssertIsValid());
145
      return FlutterLogoDecoration.lerp(a as FlutterLogoDecoration?, this, t);
146
    }
147
    return super.lerpFrom(a, t) as FlutterLogoDecoration?;
148 149 150
  }

  @override
151
  FlutterLogoDecoration? lerpTo(Decoration? b, double t) {
152
    assert(debugAssertIsValid());
153 154
    if (b == null || b is FlutterLogoDecoration) {
      assert(b == null || b.debugAssertIsValid());
155
      return FlutterLogoDecoration.lerp(this, b as FlutterLogoDecoration?, t);
156
    }
157
    return super.lerpTo(b, t) as FlutterLogoDecoration?;
158 159 160 161
  }

  @override
  // TODO(ianh): better hit testing
162
  bool hitTest(Size size, Offset position, { TextDirection? textDirection }) => true;
163 164

  @override
165
  BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
166
    assert(debugAssertIsValid());
167
    return _FlutterLogoPainter(this);
168 169
  }

170 171 172 173 174
  @override
  Path getClipPath(Rect rect, TextDirection textDirection) {
    return Path()..addRect(rect);
  }

175
  @override
176
  bool operator ==(Object other) {
177
    assert(debugAssertIsValid());
178
    if (identical(this, other)) {
179
      return true;
180
    }
181 182 183 184
    return other is FlutterLogoDecoration
        && other.textColor == textColor
        && other._position == _position
        && other._opacity == _opacity;
185 186 187 188
  }

  @override
  int get hashCode {
189
    assert(debugAssertIsValid());
190
    return Object.hash(
191
      textColor,
192
      _position,
193
      _opacity,
194 195 196 197
    );
  }

  @override
198 199
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
200
    properties.add(ColorProperty('textColor', textColor));
201
    properties.add(EnumProperty<FlutterLogoStyle>('style', style));
202
    if (_inTransition) {
203
      properties.add(DiagnosticsNode.message('transition ${debugFormatDouble(_position)}:${debugFormatDouble(_opacity)}'));
204
    }
205 206 207 208 209 210
  }
}


/// An object that paints a [BoxDecoration] into a canvas.
class _FlutterLogoPainter extends BoxPainter {
211
  _FlutterLogoPainter(this._config)
212
      : assert(_config.debugAssertIsValid()),
213
        super(null) {
214 215 216 217 218 219
    _prepareText();
  }

  final FlutterLogoDecoration _config;

  // these are configured assuming a font size of 100.0.
220
  // TODO(dnfield): Figure out how to dispose this https://github.com/flutter/flutter/issues/110601
221 222
  late TextPainter _textPainter;
  late Rect _textBoundingRect;
223 224 225

  void _prepareText() {
    const String kLabel = 'Flutter';
226 227
    _textPainter = TextPainter(
      text: TextSpan(
228
        text: kLabel,
229
        style: TextStyle(
230
          color: _config.textColor,
231 232 233
          fontFamily: 'Roboto',
          fontSize: 100.0 * 350.0 / 247.0, // 247 is the height of the F when the fontSize is 350, assuming device pixel ratio 1.0
          fontWeight: FontWeight.w300,
Ian Hickson's avatar
Ian Hickson committed
234 235 236 237
          textBaseline: TextBaseline.alphabetic,
        ),
      ),
      textDirection: TextDirection.ltr,
238 239
    );
    _textPainter.layout();
240
    final ui.TextBox textSize = _textPainter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: kLabel.length)).single;
241
    _textBoundingRect = Rect.fromLTRB(textSize.left, textSize.top, textSize.right, textSize.bottom);
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
  }

  // This class contains a lot of magic numbers. They were derived from the
  // values in the SVG files exported from the original artwork source.

  void _paintLogo(Canvas canvas, Rect rect) {
    // Our points are in a coordinate space that's 166 pixels wide and 202 pixels high.
    // First, transform the rectangle so that our coordinate space is a square 202 pixels
    // to a side, with the top left at the origin.
    canvas.save();
    canvas.translate(rect.left, rect.top);
    canvas.scale(rect.width / 202.0, rect.height / 202.0);
    // Next, offset it some more so that the 166 horizontal pixels are centered
    // in that square (as opposed to being on the left side of it). This means
    // that if we draw in the rectangle from 0,0 to 166,202, we are drawing in
    // the center of the given rect.
    canvas.translate((202.0 - 166.0) / 2.0, 0.0);

    // Set up the styles.
261
    final Paint lightPaint = Paint()
262
      ..color = const Color(0xFF54C5F8);
263
    final Paint mediumPaint = Paint()
264
      ..color = const Color(0xFF29B6F6);
265
    final Paint darkPaint = Paint()
266
      ..color = const Color(0xFF01579B);
267

268
    final ui.Gradient triangleGradient = ui.Gradient.linear(
269 270
      const Offset(87.2623 + 37.9092, 28.8384 + 123.4389),
      const Offset(42.9205 + 37.9092, 35.0952 + 123.4389),
271
      <Color>[
272 273
        const Color(0x001A237E),
        const Color(0x661A237E),
274 275
      ],
    );
276
    final Paint trianglePaint = Paint()
277
      ..shader = triangleGradient;
278 279

    // Draw the basic shape.
280
    final Path topBeam = Path()
281 282 283 284 285 286
      ..moveTo(37.7, 128.9)
      ..lineTo(9.8, 101.0)
      ..lineTo(100.4, 10.4)
      ..lineTo(156.2, 10.4);
    canvas.drawPath(topBeam, lightPaint);

287
    final Path middleBeam = Path()
288 289
      ..moveTo(156.2, 94.0)
      ..lineTo(100.4, 94.0)
290 291
      ..lineTo(78.5, 115.9)
      ..lineTo(106.4, 143.8);
292 293
    canvas.drawPath(middleBeam, lightPaint);

294
    final Path bottomBeam = Path()
295 296 297 298 299 300
      ..moveTo(79.5, 170.7)
      ..lineTo(100.4, 191.6)
      ..lineTo(156.2, 191.6)
      ..lineTo(107.4, 142.8);
    canvas.drawPath(bottomBeam, darkPaint);

301
    // The overlap between middle and bottom beam.
302
    canvas.save();
303
    canvas.transform(Float64List.fromList(const <double>[
304 305 306 307 308 309
      // careful, this is in _column_-major order
      0.7071, -0.7071, 0.0, 0.0,
      0.7071, 0.7071, 0.0, 0.0,
      0.0, 0.0, 1.0, 0.0,
      -77.697, 98.057, 0.0, 1.0,
    ]));
Dan Field's avatar
Dan Field committed
310
    canvas.drawRect(const Rect.fromLTWH(59.8, 123.1, 39.4, 39.4), mediumPaint);
311 312
    canvas.restore();

313
    // The gradients below the middle beam on top of the bottom beam.
314
    final Path triangle = Path()
315 316 317 318 319 320 321 322 323 324 325
      ..moveTo(79.5, 170.7)
      ..lineTo(120.9, 156.4)
      ..lineTo(107.4, 142.8);
    canvas.drawPath(triangle, trianglePaint);

    canvas.restore();
  }

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    offset += _config.margin.topLeft;
326
    final Size canvasSize = _config.margin.deflateSize(configuration.size!);
327
    if (canvasSize.isEmpty) {
328
      return;
329
    }
330
    final Size logoSize;
331 332 333 334 335 336 337 338 339 340
    if (_config._position > 0.0) {
      // horizontal style
      logoSize = const Size(820.0, 232.0);
    } else if (_config._position < 0.0) {
      // stacked style
      logoSize = const Size(252.0, 306.0);
    } else {
      // only the mark
      logoSize = const Size(202.0, 202.0);
    }
341
    final FittedSizes fittedSize = applyBoxFit(BoxFit.contain, logoSize, canvasSize);
342
    assert(fittedSize.source == logoSize);
343
    final Rect rect = Alignment.center.inscribe(fittedSize.destination, offset & canvasSize);
344
    final double centerSquareHeight = canvasSize.shortestSide;
345
    final Rect centerSquare = Rect.fromLTWH(
346 347 348
      offset.dx + (canvasSize.width - centerSquareHeight) / 2.0,
      offset.dy + (canvasSize.height - centerSquareHeight) / 2.0,
      centerSquareHeight,
349
      centerSquareHeight,
350 351
    );

352
    final Rect logoTargetSquare;
353 354
    if (_config._position > 0.0) {
      // horizontal style
355
      logoTargetSquare = Rect.fromLTWH(rect.left, rect.top, rect.height, rect.height);
356 357 358
    } else if (_config._position < 0.0) {
      // stacked style
      final double logoHeight = rect.height * 191.0 / 306.0;
359
      logoTargetSquare = Rect.fromLTWH(
360 361 362
        rect.left + (rect.width - logoHeight) / 2.0,
        rect.top,
        logoHeight,
363
        logoHeight,
364 365 366 367 368
      );
    } else {
      // only the mark
      logoTargetSquare = centerSquare;
    }
369
    final Rect logoSquare = Rect.lerp(centerSquare, logoTargetSquare, _config._position.abs())!;
370 371 372 373

    if (_config._opacity < 1.0) {
      canvas.saveLayer(
        offset & canvasSize,
374 375
        Paint()
          ..colorFilter = ColorFilter.mode(
376
            const Color(0xFFFFFFFF).withOpacity(_config._opacity),
377
            BlendMode.modulate,
378
          ),
379 380 381 382 383 384 385 386 387 388 389 390
      );
    }
    if (_config._position != 0.0) {
      if (_config._position > 0.0) {
        // horizontal style
        final double fontSize = 2.0 / 3.0 * logoSquare.height * (1 - (10.4 * 2.0) / 202.0);
        final double scale = fontSize / 100.0;
        final double finalLeftTextPosition = // position of text in rest position
          (256.4 / 820.0) * rect.width - // 256.4 is the distance from the left edge to the left of the F when the whole logo is 820.0 wide
          (32.0 / 350.0) * fontSize; // 32 is the distance from the text bounding box edge to the left edge of the F when the font size is 350
        final double initialLeftTextPosition = // position of text when just starting the animation
          rect.width / 2.0 - _textBoundingRect.width * scale;
391
        final Offset textOffset = Offset(
392
          rect.left + ui.lerpDouble(initialLeftTextPosition, finalLeftTextPosition, _config._position)!,
393
          rect.top + (rect.height - _textBoundingRect.height * scale) / 2.0,
394 395 396
        );
        canvas.save();
        if (_config._position < 1.0) {
397
          final Offset center = logoSquare.center;
398
          final Path path = Path()
399 400 401
            ..moveTo(center.dx, center.dy)
            ..lineTo(center.dx + rect.width, center.dy - rect.width)
            ..lineTo(center.dx + rect.width, center.dy + rect.width)
402 403 404 405 406 407 408 409 410 411 412 413
            ..close();
          canvas.clipPath(path);
        }
        canvas.translate(textOffset.dx, textOffset.dy);
        canvas.scale(scale, scale);
        _textPainter.paint(canvas, Offset.zero);
        canvas.restore();
      } else if (_config._position < 0.0) {
        // stacked style
        final double fontSize = 0.35 * logoTargetSquare.height * (1 - (10.4 * 2.0) / 202.0);
        final double scale = fontSize / 100.0;
        if (_config._position > -1.0) {
414
          // This limits what the drawRect call below is going to blend with.
415
          canvas.saveLayer(_textBoundingRect, Paint());
416 417 418 419
        } else {
          canvas.save();
        }
        canvas.translate(
420
          logoTargetSquare.center.dx - (_textBoundingRect.width * scale / 2.0),
421
          logoTargetSquare.bottom,
422 423 424 425
        );
        canvas.scale(scale, scale);
        _textPainter.paint(canvas, Offset.zero);
        if (_config._position > -1.0) {
426
          canvas.drawRect(_textBoundingRect.inflate(_textBoundingRect.width * 0.5), Paint()
427
            ..blendMode = BlendMode.modulate
428 429 430
            ..shader = ui.Gradient.linear(
              Offset(_textBoundingRect.width * -0.5, 0.0),
              Offset(_textBoundingRect.width * 1.5, 0.0),
431 432
              <Color>[const Color(0xFFFFFFFF), const Color(0xFFFFFFFF), const Color(0x00FFFFFF), const Color(0x00FFFFFF)],
              <double>[ 0.0, math.max(0.0, _config._position.abs() - 0.1), math.min(_config._position.abs() + 0.1, 1.0), 1.0 ],
433
            ),
434 435 436 437 438 439
          );
        }
        canvas.restore();
      }
    }
    _paintLogo(canvas, logoSquare);
440
    if (_config._opacity < 1.0) {
441
      canvas.restore();
442
    }
443 444
  }
}