Logo Netflix [code source]

C’est un lecteur vidéo avec une vidéo courte en lecture ?

Non, il s’agit d’une animation créée de toute pièce par Dominik Roszkowski avec Flutter.

Code source :

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:html' as html;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Netflix Logo',
      theme: ThemeData(
        primaryColor: NetflixColors.NetflixRed,
        accentColor: NetflixColors.NetflixDarkRed,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      debugShowCheckedModeBanner: false,
      home: MyHomePage(title: 'Netflix Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  AnimationController contr;
  bool playing = true;
  bool showFinishAnim = false;

  FocusNode focusNode;

  @override
  void initState() {
    super.initState();
    focusNode = FocusNode()..requestFocus();
    contr = AnimationController(
      vsync: this,
      duration: Duration(seconds: 6),
    )
      ..forward()
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed ||
            status == AnimationStatus.dismissed) {
          setState(() {
            playing = false;
          });
        }
      });
    // ..repeat();
  }

  @override
  void dispose() {
    super.dispose();
    contr.dispose();
    focusNode.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size.shortestSide;
    return RawKeyboardListener(
      focusNode: focusNode,
      onKey: (key) {
        print(key);
        if (key is RawKeyDownEvent &&
            key.logicalKey == LogicalKeyboardKey.space) {
          if (playing) {
            setState(() {
              playing = false;
            });
            contr.stop();
          } else {
            if (contr.status == AnimationStatus.completed) {
              contr.reset();
            }
            contr.forward();
            setState(() {
              playing = true;
            });
          }
        }
      },
      child: Scaffold(
        backgroundColor: Colors.black,
        body: Padding(
          padding: const EdgeInsets.only(bottom: 40.0),
          child: Stack(
            children: [
              Center(
                child: AnimatedBuilder(
                  animation: contr,
                  builder: (context, child) {
                    Animation<double> getTween(double begin, double end,
                        double intBegin, double intEnd,
                        [Curve curve = Curves.linear]) {
                      return Tween<double>(begin: begin, end: end).animate(
                        CurvedAnimation(
                          parent: contr,
                          curve: Interval(intBegin, intEnd, curve: curve),
                        ),
                      );
                    }

                    final opacity =
                        getTween(1.0, 0.0, 0.8, 1.0, Curves.easeOut);
                    final translate =
                        getTween(0.0, 0.2, 0.2, 1.0, Curves.linear);
                    final translate2 =
                        getTween(0.0, 3.0, 0.53, 1.0, Curves.easeIn);
                    final rainbowOffset = getTween(1.0, 30.0, 0.53, 1.0);
                    final scaleTween =
                        getTween(1.0, 20.0, 0.2, 0.7, Curves.easeInQuint);
                    final leftLegClip = getTween(1.0, 0.0, 0.0, 0.05);
                    final leftLegOpacity =
                        getTween(1.0, 0.0, 0.45, 0.7, Curves.ease);
                    final leftLegLinesOffset = getTween(0.0, 1.0, 0.35, 0.60);

                    final middleLegClip = getTween(0.0, 1.0, 0.05, 0.1);
                    final middleLegReverseClip = getTween(0.0, 1.0, 0.44, 0.50);

                    final middleLegOpacity =
                        getTween(1.0, 0.0, 0.40, 0.60, Curves.ease);

                    final rightLegClip = getTween(1.0, 0.0, 0.1, 0.15);
                    final rightLegReverseClip = getTween(0.0, 1.0, 0.40, 0.45);
                    final rightLegLinesOffset = getTween(0.0, 1.0, 0.30, 0.40);

                    return Opacity(
                      opacity: opacity.value,
                      child: CustomPaint(
                        painter: NetflixPainter(AnimProps(
                          scale: scaleTween.value,
                          translate: translate.value,
                          secondaryTranslate: translate2.value,
                          leftLegClip: leftLegClip.value,
                          middleLegClip:
                              middleLegClip.value - middleLegReverseClip.value,
                          rightLegClip:
                              rightLegClip.value + rightLegReverseClip.value,
                          leftLegOpacity: leftLegOpacity.value,
                          middleLegOpacity: middleLegOpacity.value,
                          rightLegLinesOffset: rightLegLinesOffset.value,
                          leftLegLinesOffset: leftLegLinesOffset.value,
                          rightLegReverseClip: rightLegReverseClip.value,
                          rainbowOffset: rainbowOffset.value,
                          showFinishAnim: this.showFinishAnim,
                        ), contr),
                        child: Container(
                          height: size,
                          width: size,
                        ),
                      ),
                    );
                  },
                ),
              ),
              Positioned(
                top: 0,
                right: 0,
                child: FlatButton.icon(
                  textColor: Colors.white,
                  icon: Icon(
                    Icons.more_vert,
                    color: Colors.white,
                  ),
                  label: Text(
                    'Experimental',
                    style: TextStyle(fontSize: 12),
                  ),
                  onPressed: () {
                    setState(() {
                      showFinishAnim = !showFinishAnim;
                    });
                  },
                ),
              ),
              Positioned(
                bottom: 0,
                left: 0,
                right: 0,
                child: Column(
                  children: [
                    ValueListenableBuilder(
                      valueListenable: contr,
                      builder: (context, value, child) => Slider(
                        min: 0.0,
                        max: 1.0,
                        activeColor: NetflixColors.NetflixRed,
                        value: value,
                        onChanged: (val) {
                          setState(() {
                            playing = false;
                          });
                          contr.stop();
                          contr.value = val;
                        },
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Flexible(
                            child: FlatButton.icon(
                              textColor: Colors.white,
                              icon: Icon(Icons.favorite_border),
                              label:
                                  Flexible(child: Text('Follow me on Twitter')),
                              onPressed: () {
                                html.window.open(
                                    'https://twitter.com/OrestesGaolin', '');
                              },
                            ),
                          ),
                          Flexible(
                            child: FlatButton.icon(
                              textColor: Colors.white,
                              icon: Icon(Icons.open_in_new),
                              label: Flexible(child: Text('More Flutter pens')),
                              onPressed: () {
                                html.window.open(
                                    'https://twitter.com/hashtag/FlutterPen',
                                    '');
                              },
                            ),
                          ),
                          Flexible(
                            child: FlatButton.icon(
                              textColor: Colors.white,
                              icon: Icon(Icons.help_outline),
                              label:
                                  Flexible(child: Text('Learn about Flutter')),
                              onPressed: () {
                                html.window.open('https://flutter.dev/', '');
                              },
                            ),
                          ),
                        ],
                      ),
                    )
                  ],
                ),
              ),
              if (!playing)
                Center(
                  child: IconButton(
                    color: Colors.white,
                    iconSize: 60,
                    icon: Icon(Icons.play_arrow),
                    onPressed: () {
                      contr.reset();
                      contr.forward();
                      setState(() {
                        playing = true;
                      });
                    },
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

class AnimProps {
  final double leftLegClip;
  final double middleLegClip;
  final double rightLegClip;
  final double leftLegOpacity;
  final double middleLegOpacity;
  final double rightLegLinesOffset;
  final double leftLegLinesOffset;
  final double scale;
  final double translate;
  final double secondaryTranslate;
  final double rightLegReverseClip;
  final double rainbowOffset;
  final bool showFinishAnim;

  AnimProps({
    this.leftLegClip,
    this.middleLegClip,
    this.rightLegClip,
    this.leftLegOpacity,
    this.middleLegOpacity,
    this.rightLegLinesOffset,
    this.leftLegLinesOffset,
    this.scale,
    this.translate,
    this.rightLegReverseClip,
    this.rainbowOffset,
    this.secondaryTranslate,
    this.showFinishAnim,
  });
}

class NetflixPainter extends CustomPainter {
  final legWidth = 355.0;
  final letterWidth = 990.0;
  final letterHeight = 1800.0;
  final bottomArcHeight = 35.0;

  final redPaint = Paint()
    ..color = NetflixColors.NetflixRed
    ..style = PaintingStyle.fill;
  final darkRredPaint = Paint()
    ..color = NetflixColors.NetflixDarkRed
    ..style = PaintingStyle.fill;

  var rainbowColors = [
    Colors.red[900],
    Colors.red[900],
    Colors.red[900],
    Colors.green[200],
    Colors.red[900],
    Colors.red[900],
    Colors.red[900],
    Colors.red[900],
    Colors.yellow,
    Colors.red[900],
    Colors.red[900],
    Colors.yellow[800],
    Colors.pink,
    Colors.red[700],
    Colors.yellow[800],
    Colors.pink[300],
    Colors.purple[200],
    Colors.yellow[800],
    Colors.red[900],
    Colors.red[900],
    Colors.red[900],
    Colors.red[900],
    Colors.pink[100],
    Colors.red[700],
    Colors.yellow[500],
    Colors.red[900],
    Colors.red[900],
    Colors.blue[200],
    Colors.red[900],
    Colors.blue[300],
    Colors.blue[200],
    Colors.red[900],
    Colors.red[900],
    Colors.blue,
    Colors.blue[400],
  ];

  final AnimProps anim;

  NetflixPainter(this.anim, AnimationController controller) : super(repaint: controller);

  @override
  void paint(Canvas canvas, Size size) {
    final scale = size.height / letterHeight;

    canvas.translate(
      size.width / 2 - letterWidth * scale / 4 - anim.translate * size.width,
      size.height / 2 - letterHeight * scale / 4 * anim.scale,
    );
    canvas.translate(-anim.secondaryTranslate * size.width, 0);
    canvas.scale(scale / 2);
    canvas.scale(anim.scale);

    canvas.save();

    final leftLegOffset = anim.leftLegClip * letterHeight;
    final rightLegOffset = anim.rightLegClip * letterHeight;
    final topLeft = Offset(0.0, leftLegOffset);
    final bottomRightFirstLeg = Offset(legWidth, letterHeight);
    final bottomRight = Offset(letterWidth, letterHeight);
    final xRightLeg = letterWidth - legWidth;
    final topLeftSecondLeg = Offset(xRightLeg, rightLegOffset);

    final leftLeg = Rect.fromPoints(topLeft, bottomRightFirstLeg);
    final rightLeg = Rect.fromPoints(topLeftSecondLeg, bottomRight);

    _clipBottom(canvas);
    _drawLeftLeg(canvas, leftLeg);
    _drawRightLeg(canvas, rightLeg, xRightLeg);
    _drawMiddlePathWithShadow(canvas, xRightLeg);
    canvas.restore();
  }

  void _clipBottom(Canvas canvas) {
    final bottomArcClipPath = Path()
      ..moveTo(0, letterHeight)
      ..quadraticBezierTo(letterWidth / 2, letterHeight - 2 * bottomArcHeight,
          letterWidth, letterHeight)
      ..lineTo(letterWidth, 0)
      ..lineTo(0, 0)
      ..lineTo(0, letterHeight);
    canvas.clipPath(bottomArcClipPath);
  }

  void _drawMiddlePathWithShadow(Canvas canvas, double xRightLeg) {
    if (anim.middleLegOpacity > 0) {
      canvas.save();
      final middleLegOffset = anim.middleLegClip * letterHeight;
      final middleLeg = Path()
        ..moveTo(0, 0)
        ..lineTo(xRightLeg, letterHeight)
        ..lineTo(letterWidth, letterHeight)
        ..lineTo(legWidth, 0)
        ..close();
      final middleLegClipPath =
          Rect.fromLTWH(0, 0, letterWidth, middleLegOffset);
      final shadowPath = Path()
        ..moveTo(20, 0)
        ..lineTo(xRightLeg - 70, letterHeight)
        ..lineTo(letterWidth - 20, letterHeight)
        ..lineTo(legWidth + 70, 0)
        ..close();
      final shadowPaint = Paint()
        ..color = Colors.black.withOpacity(anim.middleLegOpacity)
        ..maskFilter = MaskFilter.blur(BlurStyle.normal, 30);
      final middleLegPaint = Paint()
        ..color =
            NetflixColors.NetflixDarkRed.withOpacity(anim.middleLegOpacity);
      canvas.clipRect(middleLegClipPath);
      canvas.drawPath(shadowPath, shadowPaint);
      canvas.drawPath(middleLeg, middleLegPaint);
      drawMiddleLines(xRightLeg, canvas);

      canvas.restore();
    }
  }

  void drawMiddleLines(double xRightLeg, Canvas canvas) {
    if (anim.middleLegOpacity > 0) {
      final gradientPaint = LinearGradient(
        colors: [Colors.black, Colors.transparent],
        stops: [0.0, 1.0],
        begin: Alignment.bottomRight,
        end: Alignment.topCenter,
      );

      final steps = [20, 55, 80, 180, 190, 205, 280, 300];
      final stepsE = [10, 5, 10, 10, 20, 5, 10, 15];
      final start = (1 - anim.rightLegLinesOffset) * letterHeight;
      final middleLegLinePath =
          Rect.fromLTWH(0, start, letterWidth, letterHeight);
      for (var i = 0; i < steps.length; i++) {
        final xTop = 20.0 + steps[i];
        final xBottom = xRightLeg + steps[i];
        final middleLinePath = Path()
          ..moveTo(xTop, 0)
          ..lineTo(xBottom, letterHeight)
          ..lineTo(xBottom + stepsE[i], letterHeight)
          ..lineTo(xTop + stepsE[i], 0)
          ..close();
        canvas.drawPath(
          middleLinePath,
          Paint()
            ..shader = gradientPaint.createShader(middleLegLinePath)
            ..maskFilter = MaskFilter.blur(BlurStyle.outer, 3),
        );
      }
    }
  }

  void _drawRightLeg(Canvas canvas, Rect rightLeg, double xRightLeg) {
    canvas.save();
    canvas.drawRect(rightLeg, darkRredPaint);
    final gradientPaint = LinearGradient(
      colors: [Colors.black, Colors.transparent],
      stops: [0.0, 1.0],
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
    );
    final rightLegGradientOffset = letterHeight * anim.rightLegLinesOffset;
    final steps = [20, 55, 80, 180, 280, 300];
    final stepsE = [25, 60, 120, 230, 290, 340];
    for (var i = 0; i < steps.length; i++) {
      final rightShadowPath = Rect.fromLTRB(
        xRightLeg + steps[i],
        0,
        xRightLeg + stepsE[i],
        rightLegGradientOffset,
      );
      canvas.drawRect(
        rightShadowPath,
        Paint()
          ..shader = gradientPaint.createShader(rightShadowPath)
          ..maskFilter = MaskFilter.blur(BlurStyle.outer, 3),
      );
    }
    final rightLegGradient = Rect.fromLTWH(
      rightLeg.left - 2,
      rightLeg.top - 2,
      rightLeg.width + 4,
      rightLegGradientOffset - 120,
    );
    canvas.drawRect(
      rightLegGradient,
      Paint()..shader = gradientPaint.createShader(rightLegGradient),
    );
    canvas.restore();
  }

  void _drawLeftLeg(Canvas canvas, Rect leftLeg) {
    canvas.save();
    if (anim.leftLegOpacity > 0) {
      final leftLegPaint = Paint()
        ..color = NetflixColors.NetflixDarkRed.withOpacity(anim.leftLegOpacity);

      canvas.drawRect(
        leftLeg,
        leftLegPaint,
      );
    }

    if (anim.showFinishAnim == true) {
      if (anim.leftLegOpacity < 1) {
        canvas.save();

        for (var i = 0; i < 30; i++) {
          final rect = Rect.fromLTWH(
            (0.0 + i * 15.0) % legWidth * anim.rainbowOffset,
            0,
            i % 10.0,
            letterHeight,
          );
          canvas.drawRect(
            rect,
            Paint()
              ..color = rainbowColors[i % rainbowColors.length]
                  .withOpacity(1.0 - anim.leftLegOpacity)
              ..maskFilter = MaskFilter.blur(BlurStyle.normal, 3),
          );
        }
        canvas.restore();
      }
    }
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

class NetflixColors {
  static const Color NetflixRed = Color(0xFFE50914);
  static const Color NetflixDarkRed = Color(0xFFB20710);
}
1 J'aime