I am participating in the Mid-Autumn Festival Creative Submission contest, please see: Mid-Autumn Festival Creative Submission Contest for details

preface

The activities of the nuggets have participated in several times, June and August of the day more “force” code farmers work overtime every day code word, to September, to a Mid-Autumn festival creative contribution contest, do not require more, also do not require the number of articles, the result is more. Trying to be more creative on the way to work, almost hitting a telephone pole — how to be more creative? We can’t all draw cakes and Chang ‘e fly to the moon… As a “literate” island coder, let’s do something about the sea. The final effect of Flutter is shown below, using the Canvas of Flutter, source has been uploaded to Gitee: Mid-Autumn Festival creative source. The following effects are achieved:

  • The moon rises from the sea level;
  • The effect of a tidal surge;
  • A flock of wild geese fluttered southward;
  • The effect of twinkling stars in the sky;
  • As the moon rises in height, the sky becomes more and more full moon (need to look at the image carefully).

Sea rising moon tianya total at this time

The moon on the sea, the end of the world at this time. Lovers hate night, unexpectedly from the night of acacia. Extinguish candle flow light is full, put on clothes feel dew. Unbearable hand gift, but also sleep dream. — Zhang Jiuling, Looking forward to the Moon

Mid-Autumn festival, standing on the beach looking at the rising moon, for people living in a foreign land, will inevitably think of their relatives in their hometown. So how do we achieve the “rising moon” effect in Flutter? In order to draw, Canvas must be used. We will introduce the details of Canvas drawing in detail in the following chapters. Here we will give a brief introduction. Flutter provides a CustomPaint component for our custom drawing. The CustomPaint component has three important parameters:

CustomPaint(
  child: childWidget(),
  foregroundPainter: foregroundPainter(),
  painter: backgroundPainter(),
)
Copy the code
  • child:CustomPaintChild component of;
  • painterandforegroundPainterAre:CustomPainterClass for definitioncanvasThe content of the drawing. The difference is, first, executionpainterDraw command. The next step is to render on the backgroundchildThe child component. In the end,foregroundPainterThe content will be drawn inchildOn the first floor.

CustomPainter provides a paint method for drawing graphics that takes canvas and size, where canvas is the canvas and size is the canvas size. Canvas provides many methods for drawing graphics, such as drawing paths, rectangles, circles and lines. Let’s take canvas and draw the moon first. Let’s draw a separate paintMooen method with three parameters:

  • Canvas, canvasThe canvas.
  • center: The center of the moon.
  • radius: The radius of the moon, the size of the moon.

Here’s how we did it: We painted a slightly larger translucent circle on the moon to make it look like it was glowing.

void paintMooen(Canvas canvas, Offset center, double raidus) {
    varmooenPaint = Paint().. color = Colors.yellow[100]! ; mooenPaint.strokeWidth =2.0;
    canvas.drawCircle(
      center,
      raidus,
      mooenPaint,
    );

    varlightPaint = Paint().. color = Colors.yellow[100]! .withAlpha(30);
    lightPaint.strokeWidth = 2.0;
    canvas.drawCircle(
      center,
      raidus + 3,
      lightPaint,
    );
  }
Copy the code

The next step is to draw the sea. Here we simply make the background color a little brighter and then use black to represent the sea. Draw the background color and sea using the following code, where seaLevel is a member property of the custom CustomPainter class that controls the height of the seaLevel:

canvas.drawColor(Color(0xFF252525), BlendMode.color);
Copy the code
void paintSea(Canvas canvas, Size size) {
  int seaColor = 0xFF020408;
  varseaPaint = Paint().. color = Color(seaColor); seaPaint.strokeWidth =2.0;

  Path seaPath = Path();
  seaPath.moveTo(0, seaLevel);

  seaPath.lineTo(size.width, seaLevel);
  seaPath.lineTo(size.width, size.height);
  seaPath.lineTo(0, size.height);
  seaPath.lineTo(0, seaLevel);
  canvas.drawPath(seaPath, seaPaint);
}
Copy the code

Of course, we also need to write a line of poetry, which uses text drawing:

void paintPoet(Canvas canvas, String poet, Size size) {
    var style = TextStyle(
      fontWeight: FontWeight.w300,
      fontSize: 26.0,
      color: Colors.yellow[100]);finalParagraphBuilder paragraphBuilder = ParagraphBuilder( ParagraphStyle( fontSize: style.fontSize, fontFamily: style.fontFamily, fontStyle: style.fontStyle, fontWeight: style.fontWeight, textAlign: TextAlign.center, ), ) .. pushStyle(style.getTextStyle()) .. addText(poet);finalParagraph paragraph = paragraphBuilder.build() .. layout(ParagraphConstraints(width: size.width)); canvas.drawParagraph(paragraph, Offset(0.100));
  }
Copy the code

The corresponding paint method is as follows, where moonCenterY is a member property of our custom CustomPainter class that controls the position of the moon:

@override
void paint(Canvas canvas, Size size) {
  canvas.drawColor(Color(0xFF252525), BlendMode.color);
  var center = size / 2;
  paintMooen(canvas, Offset(center.width, moonCenterY), 90);
  paintSea(canvas, size);
  paintPoet(canvas, 'The moon rises and the horizon is at this time', size);
}
Copy the code

The drawing effect is as follows:

Well, it doesn’t feel much, at least let the moon rise. We can control the center position of the moon in the state management. We can use the timer to change the center position of the moon and let it rise gradually. The state management code looks like this, with three variables:

  • seaLevel: sea level;
  • finalPosition: maximum elevation of the moon after its rise;
  • _mooenCenterY: The center of the moon Y coordinate, through the timer to let this variable gradually reduced, you can let the moon rise.
class MoonStep1Controller extends GetxController {
  final double seaLevel = Get.height - 180.0;
  final double finalPosition = 300;
  late double _moonCenterY;
  get moonCenterY => _moonCenterY;
  late Timer _downcountTimer;

  @override
  void onInit() {
    _startPosition = seaLevel;
    super.onInit();
  }

  @override
  void onReady() {
    _downcountTimer = Timer.periodic(Duration(milliseconds: 40), repaint);
    super.onReady();
  }

  void repaint(Timer timer) {
    bool needUpdate = false;
    if (_moonCenterY > finalPosition) {
      _moonCenterY -= 1;
      needUpdate = true;
    } else {
      timer.cancel();
    }

    if(needUpdate) { update(); }}@override
  void onClose() {
    _downcountTimer.cancel();
    super.onClose(); }}Copy the code

With the state manager in place, we wrap the CustomPaint component with GetBuilder and we are ready to implement dynamic effects.

@override
Widget build(BuildContext context) {
  return GetBuilder<MoonStep1Controller>(
    init: controller,
    builder: (store) => CustomPaint(
      child: null,
      foregroundPainter: MoonStep1Painter(
        mooenCenterY: store.mooenCenterY,
        seaLevel: store.seaLevel,
      ),
    ),
  );
}
Copy the code

See how it looks? Even though the moon is up, it almost means that the water is too fake. Go on!

Chunjiang tide even sea flat sea moon tide health

Chunjiang tidewater sea level, sea moon tide health. Yan yan with thousands of miles, where spring without moon! — Excerpt from Spring River, Flowers and Moonlight Night by Zhang Ruoxu

Well, the tide is a little difficult, we need to create a tide effect out! How do we do that? When we look at the sea, we have a sense of the ebb and flow of the waves as they advance and retreat in layers. Therefore, you can use multiple layers of drawing, each layer rising and falling, the rhythm is not the same should have a tidal effect. Here we use 3 layers, before the 3 layers, with timer to control the amplitude of waves undulating is not the same, there will be surging effect. Let’s look at the statically drawn graphics first (use different colors to distinguish them).This is used herePathOne of the pathsarcToPointThe way you do it is you connect two points in an arc, and then you have oneclockwiseThe parameter controls whether the arc is drawn clockwise or counterclockwise. The default is clockwise. The spray effect is achieved by dividing the screen into even widths and then controlling whether the arc is drawn clockwise or counterclockwise based on odd or even numbers.

Path backPath = Path();
backPath.moveTo(0, seaLevel);
int backCount = 6;
for (var i = 0; i < backCount + 1; ++i) {
  if (i % 2= =0) {
    backPath.arcToPoint(
        Offset(size.width / backCount + i * size.width / backCount,
            seaLevel + waveY),
        radius: Radius.circular(waveRadius));
  } else {
    backPath.arcToPoint(
        Offset(size.width / backCount + i * size.width / backCount,
            seaLevel + waveY),
        radius: Radius.circular(waveRadius),
        clockwise: false); }}Copy the code

With this foundation, we can use timers to control the height of the waves to achieve the surge effect. Here, we use two variables to control the waves, one is _waveY, and the height of the waves. By adding and subtracting the timer, the values fluctuate within a certain range, so as to achieve the surge effect of different waves. The other is _waveRadius, which controls the radius of the arc. Changing the arc gives a lateral movement effect.

class MoonStep2Controller extends GetxController {
  // Omits some properties of moon rise control
  late double _waveY;
  get waveY => _waveY;
  double _waveRadius = 200.0;
  get waveRadius => _waveRadius;

  int _waveMoveCount = 0;
  final int waveMoveStep = 20;
  bool moveForward = true;
  bool first = true;
  late Timer _downcountTimer;

  @override
  void onInit() {
    _mooenCenterY = seaLevel;
    _waveY = 0;
    super.onInit();
  }

  @override
  void onReady() {
    _downcountTimer = Timer.periodic(Duration(milliseconds: 40), repaint);
    super.onReady();
  }

  void repaint(Timer timer) {
    bool needUpdate = false;
    if (_mooenCenterY > finalPosition) {
      _mooenCenterY -= 1;
      needUpdate = true;
    } else {
      timer.cancel();
    }

    _waveMoveCount++;
    int maxStep = first ? waveMoveStep : waveMoveStep * 2;
    if (moveForward) {
      _waveY += 0.5;
    } else {
      _waveY -= 0.5;
    }
    if (_waveMoveCount > maxStep) {
      _waveMoveCount = 0;
      first = false; moveForward = ! moveForward; }if(needUpdate) { update(); }}}Copy the code

Then change the method of drawing the sea surface as follows:

void paintSea(Canvas canvas, Size size) {
  varseaPaint = Paint().. color = Colors.blue; seaPaint.strokeWidth =2.0;
  Path backPath = Path();
  backPath.moveTo(0, seaLevel);
  int backCount = 6;
  double waveRadius = 200.0;
  for (var i = 0; i < backCount + 1; ++i) {
    if (i % 2= =0) {
      backPath.arcToPoint(
          Offset(size.width / backCount + i * size.width / backCount,
              seaLevel + waveY),
          radius: Radius.circular(waveRadius));
    } else {
      backPath.arcToPoint(
          Offset(size.width / backCount + i * size.width / backCount,
              seaLevel + waveY),
          radius: Radius.circular(waveRadius),
          clockwise: false);
    }
  }
  backPath.lineTo(size.width, size.height);
  backPath.lineTo(0, size.height); canvas.drawPath(backPath, seaPaint); seaPaint.. color = Colors.green; Path middlePath = Path(); middlePath.moveTo(size.width, seaLevel);int middleWaveCount = 4;
  for (var i = 1; i < middleWaveCount + 1; ++i) {
    if (i % 2= =0) {
      middlePath.arcToPoint(
          Offset(size.width - i * size.width / middleWaveCount,
              seaLevel - waveY),
          radius: Radius.circular(waveRadius),
          clockwise: false);
    } else {
      middlePath.arcToPoint(
          Offset(size.width - i * size.width / middleWaveCount,
              seaLevel - waveY),
          radius: Radius.circular(waveRadius),
          clockwise: true);
    }
  }
  middlePath.lineTo(0, size.height); middlePath.lineTo(size.width, size.height); canvas.drawPath(middlePath, seaPaint); Path frontPath = Path(); seaPaint.. color = Colors.red; frontPath.moveTo(0, seaLevel);
  int frondCount = 8;
  for (var i = 0; i < frondCount + 1; ++i) {
    if (i % 2= =0) {
      frontPath.arcToPoint(
          Offset(size.width / frondCount + i * size.width / frondCount,
              seaLevel + waveY + 10),
          radius: Radius.circular(waveRadius),
          clockwise: false);
    } else {
      frontPath.arcToPoint(
          Offset(size.width / frondCount + i * size.width / frondCount,
              seaLevel + waveY + 10),
          radius: Radius.circular(waveRadius),
          clockwise: true);
    }
  }
  frontPath.lineTo(size.width, size.height);
  frontPath.lineTo(0, size.height);
  canvas.drawPath(frontPath, seaPaint);
}
Copy the code

Now what about the effect? Tidal surge effect is out! But it’s still a little monotonous.

The magpies flew south

When the moon is bright and the stars are thin, black magpies fly south. Three turns round the tree, what branch can be relied on? — Cao Cao “Short Song Line”

Mid-Autumn day, bright moon, how can there be no star company? And, who knows, we’ll be able to see geese heading south? Let’s see how to draw stars and geese.

Draw a star

The stars can actually be drawn with four arcs like the one below. Then adjust the radius of the arc to achieve the effect of drawing stars.

Path starPath = Path();
starPath.moveTo(centerX, centerY - offset);
starPath.arcToPoint(Offset(centerX - offset, centerY),
    radius: Radius.circular(radius));
starPath.arcToPoint(Offset(centerX, centerY + offset),
    radius: Radius.circular(radius));
starPath.arcToPoint(Offset(centerX + offset, centerY),
    radius: Radius.circular(radius));
starPath.arcToPoint(Offset(centerX, centerY - offset),
    radius: Radius.circular(radius));
canvas.drawPath(starPath, starPaint);
Copy the code

Draw the geese

The geese are actually drawn in four arcs, with a pair of wings on the left and right. After adjusting the radius of the arc, you can call out the appearance of the geese. Here we draw the left and right wings with two closed arcs.

Path leftPath = Path(); leftPath.moveTo(center.dx - wingSize, center.dy - wingHeight); leftPath .. arcToPoint(Offset(center.dx, center.dy), radius: Radius.circular(radius), clockwise:true);
leftPath.arcToPoint(Offset(center.dx - wingSize, center.dy - wingHeight),
    radius: Radius.circular(radius - wingRadiusDiff), clockwise: false); canvas.drawPath(leftPath, birdPaint); Path rightPath = Path(); rightPath.moveTo(center.dx + wingSize, center.dy - wingHeight); rightPath .. arcToPoint(Offset(center.dx, center.dy), radius: Radius.circular(radius), clockwise:false);
rightPath.arcToPoint(Offset(center.dx + wingSize, center.dy - wingHeight),
    radius: Radius.circular(radius - wingRadiusDiff), clockwise: true);
canvas.drawPath(rightPath, birdPaint);
Copy the code

Of course. Geese need groups, starlight needs to be all over the sky, this is easy to do, we use circular position on the line.

A flock of wild geese flew south

A group of geese we control the X and Y coordinates of different geese, and the size of the geese flying in the front is smaller, so that it will be more realistic. Then we can achieve the effect of the geese flying by modifying the difference of the coordinate points drawn by the wings. Let’s draw five geese. The radius in the loop represents the arc of the wing. Here, adjust a little arc according to the size of the goose to make the wing swing more realistic. WingHeight controls the height of the wings, while wingSize controls the width of the wings. The effect of wing waving can be realized by different heights, and different widths will appear different sizes of geese. BirdFlyDistance represents the flying distance of wild geese. Since we are vertical, the value in the vertical direction is larger, so the value in the vertical direction is multiplied by 2 to make the vertical speed faster. These parameters are controlled by state management timers,

 void paint(Canvas canvas, Size size) {
  / /...
  for (var i = 0; i < 5; ++i) {
    double step = (5.0 - i) * 15;
    paintBird(
        canvas,
        Offset(birdFlyDistance + step,
            seaLevel + 100 - birdFlyDistance * 2 - step),
        wingHeight,
        radius: 20.0 + 2 * i,
        wingSize: 22.0 + 2 * i);
  }
   / /...
 }


Copy the code

A star-studded

Instead of drawing stars in a row like geese, we need random positions and random sizes. In addition, we can control the transparency of some stars randomly to achieve the effect of twinkling, and finally achieve the effect of starry sky. Since it is redrawn each time, the position and size of the star must be set at initialization to ensure that the position of the star remains the same, and each drawing is just a random flicker.

class SeaMooenController extends GetxController {
  / /...
	late List<Offset> _starPositions;
  get starPositions => _starPositions;
  late List<double> _starSizes;
  get starSizes => _starSizes;
  late List<bool> _blinkIndexes;
  get blinkIndexes => _blinkIndexes;
  
  @override
  void onInit() {
    // ...
    // The star is drawn at a point above sea level
    var starStartPosition = seaLevel - 20.0;
    // Random star positions
    _starPositions = List.generate(
      starCount,
      (index) => Offset(
        4.0 + Random().nextInt(Get.width.toInt() - 4),
        starStartPosition - Random().nextInt(starStartPosition.toInt()),
      ),
    );
    // Random star size
    _starSizes =
        List.generate(starCount, (index) => Random().nextInt(10) / 3.0);
    _blinkIndexes = List.generate(
        starCount, (index) => Random().nextInt(starCount) % 3= =0);
    super.onInit();
  }
  
  @override
  void onReady() {
    _downcountTimer = Timer.periodic(Duration(milliseconds: 40), repaint);
    super.onReady();
  }

  void repaint(Timer timer) {
    / /...
    _waveMoveCount++;
    _birdFlyDistance += 0.5;
    int maxStep = first ? waveMoveStep : waveMoveStep * 2;

    if (moveForward) {
      _waveY += 0.5;
      _wingHeight -= 0.3;
    } else {
      _waveY -= 0.5;
      _wingHeight += 0.3;
    }
    if (_waveMoveCount > maxStep) {
      _waveMoveCount = 0;
      first = false; moveForward = ! moveForward; }// The stars twinkle randomly
    _blinkIndexes = List.generate(
        starCount, (index) => Random().nextInt(starCount) % 2= =0);

    / /...}}Copy the code

:

The moon rise on the surface, we should put the sky light, step by step so that there will be a feeling of moonlight light up the sky, this is very simple, we according to the transparency of the height of the moon change the background color, appear more black initially high transparency, transparency falling behind, will appear white, thus feel brighter.

anvas.drawColor(
  Color(0xFF252525)
      .withAlpha(250 - (moonCenterY / size.height * 255).toInt()),
Copy the code

The final result

The final effect is the same as the beginning of the GIF, listening to the tide surging in the seaside, watching the disc like the moon slowly rising in the sea, the stars are shining, a flock of wild geese flying, passing the moon… Wouldn’t it be super romantic to have someone with you?

May we share the beauty of this graceful moonlight for a long time

People have joys and sorrows, the moon waxes and wanes, this matter is difficult to complete. We wish each other a long life so as to share the beauty of this graceful moonlight, even though miles apart. — Excerpts from “Water Tune Song Head ยท Bright moon” by Su Shi

The Mid-Autumn Festival is coming. I wish you all a happy and healthy family — I wish you a long life and a beautiful moonlight.

I am dao Code Farmer with the same name as my wechat official account. This is a column about the introduction and practice of Flutter, providing systematic learning articles about Flutter. See the corresponding source code here: The source code of Flutter Introduction and Practical column. If you have any questions, please add me to the wechat account: island-coder.

๐Ÿ‘๐Ÿป : feel the harvest please point a praise to encourage!

๐ŸŒŸ : Collect articles, easy to look back!

๐Ÿ’ฌ : Comment exchange, mutual progress!