ফ্লটারে পরবর্তী প্রজন্মের UI তৈরি করা

1. আপনি শুরু করার আগে

হট রিলোড এবং ঘোষণামূলক UI এর সংমিশ্রণ ব্যবহার করে পুনরাবৃত্তভাবে নতুন ব্যবহারকারী ইন্টারফেস তৈরি করতে ডেভেলপারদের সক্ষম করতে ফ্লটার দুর্দান্ত। যাইহোক, এমন একটি সময় আসে যখন আপনাকে একটি ইন্টারফেসে অতিরিক্ত ইন্টারঅ্যাক্টিভিটি যুক্ত করতে হবে। এই ছোঁয়াগুলি হোভারে একটি বোতাম অ্যানিমেট করার মতো সহজ বা শেডারের মতো নাটকীয় হতে পারে যা GPU-এর শক্তি ব্যবহার করে ব্যবহারকারীর ইন্টারফেসকে বিকৃত করে।

এই কোডল্যাবে, আপনি একটি ফ্লাটার অ্যাপ তৈরি করবেন যা অ্যানিমেশন, শেডার এবং কণা ক্ষেত্রের শক্তি ব্যবহার করে একটি ইউজার ইন্টারফেস তৈরি করে যা সেই কল্পবিজ্ঞান চলচ্চিত্র এবং টিভি শোগুলিকে উদ্ভাসিত করে যা আমরা সবাই দেখতে পছন্দ করি যখন আমরা কোডিং করি না।

আপনি কি নির্মাণ করবেন

আপনি একটি পোস্ট অ্যাপোক্যালিপটিক সাই-ফাই থিমযুক্ত গেমের জন্য প্রাথমিক মেনু পৃষ্ঠা তৈরি করবেন। একটি ফ্র্যাগমেন্ট শেডার সহ একটি শিরোনাম রয়েছে যা পাঠ্যটিকে দৃশ্যমানভাবে অ্যানিমেট করার জন্য নমুনা দেয়, একটি অসুবিধা মেনু যা পৃষ্ঠার রঙের থিমকে প্রচুর অ্যানিমেশনের সাথে পরিবর্তন করে এবং একটি দ্বিতীয় খণ্ড শেডার দিয়ে আঁকা একটি অ্যানিমেটেড অরব। যদি তা যথেষ্ট না হয়, কোডল্যাবের শেষে আপনি পৃষ্ঠায় গতিশীলতা এবং আগ্রহ আনতে একটি সূক্ষ্ম কণা প্রভাব যুক্ত করবেন।

নিম্নলিখিত স্ক্রিনশটগুলি দেখায় যে অ্যাপটি আপনি তিনটি সমর্থিত ডেস্কটপ অপারেটিং সিস্টেমে তৈরি করবেন: Windows, Linux এবং macOS৷ সম্পূর্ণতার জন্য, একটি ওয়েব ব্রাউজার সংস্করণ (এছাড়াও সমর্থিত) প্রদান করা হয়। সর্বত্র অ্যানিমেশন এবং টুকরা shaders!

উইন্ডোজে চলমান সমাপ্ত অ্যাপ

ক্রোম ব্রাউজারে চলমান সমাপ্ত অ্যাপ

লিনাক্সে চলমান সমাপ্ত অ্যাপ

macOS-এ চলমান সমাপ্ত অ্যাপ

পূর্বশর্ত

আপনি কি শিখবেন

আপনি কি প্রয়োজন হবে

2. শুরু করুন

স্টার্টার কোড ডাউনলোড করুন

  1. এই GitHub সংগ্রহস্থলে নেভিগেট করুন।
  2. এই কোডল্যাবের জন্য সমস্ত কোড ডাউনলোড করতে কোড > জিপ ডাউনলোড করুন ক্লিক করুন।
  3. একটি codelabs-main রুট ফোল্ডার আনপ্যাক করতে ডাউনলোড করা জিপ ফাইলটি আনজিপ করুন। আপনার শুধুমাত্র next-gen-ui/ সাবডিরেক্টরি দরকার, যেটিতে step_01 থেকে step_06 ফোল্ডার রয়েছে, যেটিতে সোর্স কোড রয়েছে যা আপনি এই কোডল্যাবের প্রতিটি ধাপের জন্য তৈরি করেন।

প্রকল্প নির্ভরতা ডাউনলোড করুন

  1. VS কোডে, স্টার্টার প্রজেক্ট খুলতে File > Open ফোল্ডার > codelabs-main > next-gen-uis > step_01 এ ক্লিক করুন।
  2. আপনি যদি একটি VS কোড ডায়ালগ দেখতে পান যা আপনাকে স্টার্টার অ্যাপের জন্য প্রয়োজনীয় প্যাকেজগুলি ডাউনলোড করতে অনুরোধ করে, তাহলে প্যাকেজ পান এ ক্লিক করুন।

VS কোড ডায়ালগ যা আপনাকে স্টার্টার অ্যাপের জন্য প্রয়োজনীয় প্যাকেজ ডাউনলোড করতে অনুরোধ করে।

  1. আপনি যদি একটি VS কোড ডায়ালগ দেখতে না পান যা আপনাকে স্টার্টার অ্যাপের জন্য প্রয়োজনীয় প্যাকেজগুলি ডাউনলোড করতে অনুরোধ করে, আপনার টার্মিনাল খুলুন এবং তারপর step_01 ফোল্ডারে নেভিগেট করুন এবং flutter pub get কমান্ড চালান৷

স্টার্টার অ্যাপটি চালান

  1. VS কোডে, হয় আপনি যে ডেস্কটপ অপারেটিং সিস্টেমটি চালাচ্ছেন সেটি নির্বাচন করুন অথবা আপনি যদি কোনো ওয়েব ব্রাউজারে আপনার অ্যাপটি পরীক্ষা করতে চান তাহলে Chrome।

উদাহরণস্বরূপ, আপনি যখন আপনার স্থাপনার লক্ষ্য হিসাবে macOS ব্যবহার করেন তখন আপনি যা দেখেন তা এখানে:

VSCode স্ট্যাটাস বার সাজসজ্জা যা ফ্লটার টার্গেট দেখাচ্ছে ম্যাকওএস (ডারউইন)

আপনি যখন আপনার স্থাপনার লক্ষ্য হিসাবে Chrome ব্যবহার করেন তখন আপনি যা দেখেন তা এখানে:

VSCode স্ট্যাটাস বার সজ্জা ফ্লটার টার্গেট দেখাচ্ছে Chrome (ওয়েব-জাভাস্ক্রিপ্ট)

  1. lib/main.dart ফাইলটি খুলুন এবং ক্লিক করুন VSCode থেকে প্লে বোতাম ডিবাগিং শুরু করুন । অ্যাপটি আপনার ডেস্কটপ অপারেটিং সিস্টেমে বা একটি Chrome ব্রাউজারে লঞ্চ হয়।

স্টার্টার অ্যাপটি দেখুন

স্টার্টার অ্যাপে, নিম্নলিখিতগুলি লক্ষ্য করুন:

  • UI আপনার তৈরি করার জন্য প্রস্তুত।
  • assets ডিরেক্টরিতে শিল্প সম্পদ এবং দুটি টুকরো শেডার রয়েছে যা আপনি ব্যবহার করবেন।
  • pubspec.yaml ফাইলটি ইতিমধ্যেই সম্পদ এবং পাব প্যাকেজের একটি সংগ্রহের তালিকা করে যা আপনি ব্যবহার করবেন।
  • lib ডিরেক্টরিতে বাধ্যতামূলক main.dart ফাইল রয়েছে, একটি assets.dart ফাইল যা আর্ট অ্যাসেট এবং ফ্র্যাগমেন্ট শেডারগুলির পথ তালিকাভুক্ত করে এবং একটি styles.dart ফাইল যা আপনি ব্যবহার করবেন এমন TextStyles এবং রঙগুলি তালিকাভুক্ত করে৷
  • lib ডিরেক্টরিতে একটি common ডিরেক্টরিও রয়েছে, যা এই কোডল্যাবে আপনি ব্যবহার করবেন এমন কিছু দরকারী ইউটিলিটি ধারণ করে এবং orb_shader ডিরেক্টরিতে একটি Widget রয়েছে যা একটি vertex shader সহ orb প্রদর্শন করতে ব্যবহৃত হবে।

আপনি অ্যাপটি শুরু করার পরে আপনি যা দেখতে পাবেন তা এখানে।

কোডল্যাব অ্যাপটি 'Insert Next-generation UI Here...' শিরোনামে চলছে

3. দৃশ্য আঁকা

এই ধাপে আপনি পর্দায় সমস্ত পটভূমি শিল্প সম্পদ স্তরে রাখুন। প্রথমে এটি অদ্ভুতভাবে একরঙা প্রদর্শিত হবে বলে আশা করুন, কিন্তু আপনি এই ধাপের শেষে দৃশ্যে রঙ যোগ করবেন।

দৃশ্যে সম্পদ যোগ করুন

  1. আপনার lib ডিরেক্টরিতে একটি title_screen ডিরেক্টরি তৈরি করুন এবং তারপর একটি title_screen.dart ফাইল যোগ করুন। ফাইলে নিম্নলিখিত বিষয়বস্তু যোগ করুন:

lib/title_screen/title_screen.dart

import 'package:flutter/material.dart';

import '../assets.dart';

class TitleScreen extends StatelessWidget {
  const TitleScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Stack(
          children: [
            /// Bg-Base
            Image.asset(AssetPaths.titleBgBase),

            /// Bg-Receive
            Image.asset(AssetPaths.titleBgReceive),

            /// Mg-Base
            Image.asset(AssetPaths.titleMgBase),

            /// Mg-Receive
            Image.asset(AssetPaths.titleMgReceive),

            /// Mg-Emit
            Image.asset(AssetPaths.titleMgEmit),

            /// Fg-Rocks
            Image.asset(AssetPaths.titleFgBase),

            /// Fg-Receive
            Image.asset(AssetPaths.titleFgReceive),

            /// Fg-Emit
            Image.asset(AssetPaths.titleFgEmit),
          ],
        ),
      ),
    );
  }
}

এই উইজেটে স্তরে স্ট্যাক করা সম্পদ সহ দৃশ্য রয়েছে। ব্যাকগ্রাউন্ড, মিডগ্রাউন্ড এবং ফোরগ্রাউন্ড লেয়ার দুটি বা তিনটি ইমেজের একটি গ্রুপ দ্বারা উপস্থাপিত হয়। দৃশ্যের মধ্য দিয়ে আলো কীভাবে চলে তা ক্যাপচার করতে এই চিত্রগুলিকে বিভিন্ন রঙ দিয়ে আলোকিত করা হবে।

  1. main.dart ফাইলে, নিম্নলিখিত বিষয়বস্তু যোগ করুন:

lib/main.dart

import 'dart:io' show Platform;

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:window_size/window_size.dart';
                                                          // Remove 'styles.dart' import
import 'title_screen/title_screen.dart';                  // Add this import


void main() {
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
    WidgetsFlutterBinding.ensureInitialized();
    setWindowMinSize(const Size(800, 500));
  }
  runApp(const NextGenApp());
}

class NextGenApp extends StatelessWidget {
  const NextGenApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      themeMode: ThemeMode.dark,
      darkTheme: ThemeData(brightness: Brightness.dark),
      home: const TitleScreen(),                          // Replace with this widget
    );
  }
}

এটি শিল্প সম্পদ তৈরি করে এমন একরঙা দৃশ্যের সাথে অ্যাপের UI প্রতিস্থাপন করে। এর পরে, আপনি প্রতিটি স্তর রঙ করুন।

কোডল্যাব অ্যাপটি শুধুমাত্র ব্যাকগ্রাউন্ড, মিডগ্রাউন্ড এবং ফোরগ্রাউন্ড আর্ট অ্যাসেটের সাথে চলছে, একরঙাতে প্রদর্শিত।

একটি ইমেজ কালারিং ইউটিলিটি যোগ করুন

title_screen.dart ফাইলে নিম্নলিখিত বিষয়বস্তু যোগ করে একটি ইমেজ কালারিং ইউটিলিটি যোগ করুন:

lib/title_screen/title_screen.dart

import 'package:flutter/material.dart';

import '../assets.dart';

class TitleScreen extends StatelessWidget {
  const TitleScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Stack(
          children: [
            /// Bg-Base
            Image.asset(AssetPaths.titleBgBase),

            /// Bg-Receive
            Image.asset(AssetPaths.titleBgReceive),

            /// Mg-Base
            Image.asset(AssetPaths.titleMgBase),

            /// Mg-Receive
            Image.asset(AssetPaths.titleMgReceive),

            /// Mg-Emit
            Image.asset(AssetPaths.titleMgEmit),

            /// Fg-Rocks
            Image.asset(AssetPaths.titleFgBase),

            /// Fg-Receive
            Image.asset(AssetPaths.titleFgReceive),

            /// Fg-Emit
            Image.asset(AssetPaths.titleFgEmit),
          ],
        ),
      ),
    );
  }
}

class _LitImage extends StatelessWidget {                 // Add from here...
  const _LitImage({
    required this.color,
    required this.imgSrc,
    required this.lightAmt,
  });
  final Color color;
  final String imgSrc;
  final double lightAmt;

  @override
  Widget build(BuildContext context) {
    final hsl = HSLColor.fromColor(color);
    return Image.asset(
      imgSrc,
      color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
      colorBlendMode: BlendMode.modulate,
    );
  }
}                                                         // to here.

এই _LitImage ইউটিলিটি উইজেট প্রতিটি শিল্প সম্পদকে পুনরায় রঙ করে, তারা আলো নির্গত করছে বা গ্রহণ করছে তার উপর নির্ভর করে। এটি একটি লিন্টার সতর্কতা ট্রিগার করতে পারে, যেহেতু আপনি এখনও এই নতুন উইজেটটি ব্যবহার করছেন না৷

রঙে আঁকা

title_screen.dart ফাইলটি পরিবর্তন করে রঙে পেইন্ট করুন, নিম্নরূপ:

lib/title_screen/title_screen.dart

import 'package:flutter/material.dart';

import '../assets.dart';
import '../styles.dart';                                  // Add this import

class TitleScreen extends StatelessWidget {
  const TitleScreen({super.key});

  final _finalReceiveLightAmt = 0.7;                      // Add this attribute
  final _finalEmitLightAmt = 0.5;                         // And this attribute

  @override
  Widget build(BuildContext context) {
    final orbColor = AppColors.orbColors[0];              // Add this final variable
    final emitColor = AppColors.emitColors[0];            // And this one

    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Stack(
          children: [
            /// Bg-Base
            Image.asset(AssetPaths.titleBgBase),

            /// Bg-Receive
            _LitImage(                                    // Modify from here...
              color: orbColor,
              imgSrc: AssetPaths.titleBgReceive,
              lightAmt: _finalReceiveLightAmt,
            ),                                            // to here.

            /// Mg-Base
            _LitImage(                                    // Modify from here...
              imgSrc: AssetPaths.titleMgBase,
              color: orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),                                            // to here.

            /// Mg-Receive
            _LitImage(                                    // Modify from here...
              imgSrc: AssetPaths.titleMgReceive,
              color: orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),                                            // to here.

            /// Mg-Emit
            _LitImage(                                    // Modify from here...
              imgSrc: AssetPaths.titleMgEmit,
              color: emitColor,
              lightAmt: _finalEmitLightAmt,
            ),                                            // to here.

            /// Fg-Rocks
            Image.asset(AssetPaths.titleFgBase),

            /// Fg-Receive
            _LitImage(                                    // Modify from here...
              imgSrc: AssetPaths.titleFgReceive,
              color: orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),                                            // to here.

            /// Fg-Emit
            _LitImage(                                    // Modify from here...
              imgSrc: AssetPaths.titleFgEmit,
              color: emitColor,
              lightAmt: _finalEmitLightAmt,
            ),                                            // to here.
          ],
        ),
      ),
    );
  }
}

class _LitImage extends StatelessWidget {
  const _LitImage({
    required this.color,
    required this.imgSrc,
    required this.lightAmt,
  });
  final Color color;
  final String imgSrc;
  final double lightAmt;

  @override
  Widget build(BuildContext context) {
    final hsl = HSLColor.fromColor(color);
    return Image.asset(
      imgSrc,
      color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
      colorBlendMode: BlendMode.modulate,
    );
  }
}

এখানে আবার অ্যাপটি, এই সময় শিল্প সম্পদের সাথে সবুজ রঙ করা হয়েছে।

কোডল্যাব অ্যাপটি শিল্প সম্পদের সাথে চলছে, সবুজ রঙের।

4. একটি UI যোগ করুন

এই ধাপে আপনি আগের ধাপে তৈরি দৃশ্যের উপর একটি ইউজার ইন্টারফেস রাখুন। এর মধ্যে রয়েছে শিরোনাম, অসুবিধা-নির্বাচক বোতাম এবং সব-গুরুত্বপূর্ণ স্টার্ট বোতাম।

একটি শিরোনাম যোগ করুন

  1. lib/title_screen ডিরেক্টরির ভিতরে একটি title_screen_ui.dart ফাইল তৈরি করুন এবং ফাইলটিতে নিম্নলিখিত বিষয়বস্তু যোগ করুন:

lib/title_screen/title_screen_ui.dart

import 'package:extra_alignments/extra_alignments.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';

import '../assets.dart';
import '../common/ui_scaler.dart';
import '../styles.dart';

class TitleScreenUi extends StatelessWidget {
  const TitleScreenUi({
    super.key,
  });
  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.symmetric(vertical: 40, horizontal: 50),
      child: Stack(
        children: [
          /// Title Text
          TopLeft(
            child: UiScaler(
              alignment: Alignment.topLeft,
              child: _TitleText(),
            ),
          ),
        ],
      ),
    );
  }
}

class _TitleText extends StatelessWidget {
  const _TitleText();

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Gap(20),
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Transform.translate(
              offset: Offset(-(TextStyles.h1.letterSpacing! * .5), 0),
              child: Text('OUTPOST', style: TextStyles.h1),
            ),
            Image.asset(AssetPaths.titleSelectedLeft, height: 65),
            Text('57', style: TextStyles.h2),
            Image.asset(AssetPaths.titleSelectedRight, height: 65),
          ],
        ),
        Text('INTO THE UNKNOWN', style: TextStyles.h3),
      ],
    );
  }
}

এই উইজেটে শিরোনাম এবং সমস্ত বোতাম রয়েছে যা এই অ্যাপের জন্য ব্যবহারকারী ইন্টারফেস তৈরি করে।

  1. lib/title_screen/title_screen.dart ফাইলটি আপডেট করুন, নিম্নরূপ:

lib/title_screen/title_screen.dart

import 'package:flutter/material.dart';

import '../assets.dart';
import '../styles.dart';
import 'title_screen_ui.dart';                            // Add this import

class TitleScreen extends StatelessWidget {
  const TitleScreen({super.key});

  final _finalReceiveLightAmt = 0.7;
  final _finalEmitLightAmt = 0.5;

  @override
  Widget build(BuildContext context) {
    final orbColor = AppColors.orbColors[0];
    final emitColor = AppColors.emitColors[0];

    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Stack(
          children: [
            /// Bg-Base
            Image.asset(AssetPaths.titleBgBase),

            /// Bg-Receive
            _LitImage(
              color: orbColor,
              imgSrc: AssetPaths.titleBgReceive,
              lightAmt: _finalReceiveLightAmt,
            ),

            /// Mg-Base
            _LitImage(
              imgSrc: AssetPaths.titleMgBase,
              color: orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),

            /// Mg-Receive
            _LitImage(
              imgSrc: AssetPaths.titleMgReceive,
              color: orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),

            /// Mg-Emit
            _LitImage(
              imgSrc: AssetPaths.titleMgEmit,
              color: emitColor,
              lightAmt: _finalEmitLightAmt,
            ),

            /// Fg-Rocks
            Image.asset(AssetPaths.titleFgBase),

            /// Fg-Receive
            _LitImage(
              imgSrc: AssetPaths.titleFgReceive,
              color: orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),

            /// Fg-Emit
            _LitImage(
              imgSrc: AssetPaths.titleFgEmit,
              color: emitColor,
              lightAmt: _finalEmitLightAmt,
            ),

            /// UI
            const Positioned.fill(                        // Add from here...
              child: TitleScreenUi(),
            ),                                            // to here.
          ],
        ),
      ),
    );
  }
}

class _LitImage extends StatelessWidget {
  const _LitImage({
    required this.color,
    required this.imgSrc,
    required this.lightAmt,
  });
  final Color color;
  final String imgSrc;
  final double lightAmt;

  @override
  Widget build(BuildContext context) {
    final hsl = HSLColor.fromColor(color);
    return Image.asset(
      imgSrc,
      color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
      colorBlendMode: BlendMode.modulate,
    );
  }
}

এই কোডটি চালানো শিরোনামটি প্রকাশ করে, যা ইউজার ইন্টারফেসের শুরু।

কোডল্যাব অ্যাপটি 'আউটপোস্ট [৫৭] ইনটু দ্য অজানা' শিরোনাম সহ চলছে

অসুবিধা বোতাম যোগ করুন

  1. focusable_control_builder প্যাকেজের জন্য একটি নতুন আমদানি যোগ করে title_screen_ui.dart আপডেট করুন:

lib/title_screen/title_screen_ui.dart

import 'package:extra_alignments/extra_alignments.dart';
import 'package:flutter/material.dart';
import 'package:focusable_control_builder/focusable_control_builder.dart'; // Add import
import 'package:gap/gap.dart';

import '../assets.dart';
import '../common/ui_scaler.dart';
import '../styles.dart';
  1. TitleScreenUi উইজেটে, নিম্নলিখিত যোগ করুন:

lib/title_screen/title_screen_ui.dart

class TitleScreenUi extends StatelessWidget {
  const TitleScreenUi({
    super.key,
    required this.difficulty,                            // Edit from here...
    required this.onDifficultyPressed,
    required this.onDifficultyFocused,
  });

  final int difficulty;
  final void Function(int difficulty) onDifficultyPressed;
  final void Function(int? difficulty) onDifficultyFocused; // to here.

  @override
  Widget build(BuildContext context) {
    return Padding(                                      // Move this const...
      padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), // to here.
      child: Stack(
        children: [
          /// Title Text
          const TopLeft(                                 // Add a const here, as well
            child: UiScaler(
              alignment: Alignment.topLeft,
              child: _TitleText(),
            ),
          ),

          /// Difficulty Btns
          BottomLeft(                                    // Add from here...
            child: UiScaler(
              alignment: Alignment.bottomLeft,
              child: _DifficultyBtns(
                difficulty: difficulty,
                onDifficultyPressed: onDifficultyPressed,
                onDifficultyFocused: onDifficultyFocused,
              ),
            ),
          ),                                             // to here.
        ],
      ),
    );
  }
}
  1. অসুবিধা বোতাম বাস্তবায়ন করতে নিম্নলিখিত দুটি উইজেট যোগ করুন:

lib/title_screen/title_screen_ui.dart

class _DifficultyBtns extends StatelessWidget {
  const _DifficultyBtns({
    required this.difficulty,
    required this.onDifficultyPressed,
    required this.onDifficultyFocused,
  });

  final int difficulty;
  final void Function(int difficulty) onDifficultyPressed;
  final void Function(int? difficulty) onDifficultyFocused;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        _DifficultyBtn(
          label: 'Casual',
          selected: difficulty == 0,
          onPressed: () => onDifficultyPressed(0),
          onHover: (over) => onDifficultyFocused(over ? 0 : null),
        ),
        _DifficultyBtn(
          label: 'Normal',
          selected: difficulty == 1,
          onPressed: () => onDifficultyPressed(1),
          onHover: (over) => onDifficultyFocused(over ? 1 : null),
        ),
        _DifficultyBtn(
          label: 'Hardcore',
          selected: difficulty == 2,
          onPressed: () => onDifficultyPressed(2),
          onHover: (over) => onDifficultyFocused(over ? 2 : null),
        ),
        const Gap(20),
      ],
    );
  }
}

class _DifficultyBtn extends StatelessWidget {
  const _DifficultyBtn({
    required this.selected,
    required this.onPressed,
    required this.onHover,
    required this.label,
  });
  final String label;
  final bool selected;
  final VoidCallback onPressed;
  final void Function(bool hasFocus) onHover;

  @override
  Widget build(BuildContext context) {
    return FocusableControlBuilder(
      onPressed: onPressed,
      onHoverChanged: (_, state) => onHover.call(state.isHovered),
      builder: (_, state) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: SizedBox(
            width: 250,
            height: 60,
            child: Stack(
              children: [
                /// Bg with fill and outline
                Container(
                  decoration: BoxDecoration(
                    color: const Color(0xFF00D1FF).withOpacity(.1),
                    border: Border.all(color: Colors.white, width: 5),
                  ),
                ),

                if (state.isHovered || state.isFocused) ...[
                  Container(
                    decoration: BoxDecoration(
                      color: const Color(0xFF00D1FF).withOpacity(.1),
                    ),
                  ),
                ],

                /// cross-hairs (selected state)
                if (selected) ...[
                  CenterLeft(
                    child: Image.asset(AssetPaths.titleSelectedLeft),
                  ),
                  CenterRight(
                    child: Image.asset(AssetPaths.titleSelectedRight),
                  ),
                ],

                /// Label
                Center(
                  child: Text(label.toUpperCase(), style: TextStyles.btn),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}
  1. TitleScreen উইজেটটিকে স্টেটলেস থেকে স্টেটফুল এ রূপান্তর করুন এবং অসুবিধার উপর ভিত্তি করে রঙের স্কিম পরিবর্তন করতে সক্ষম করতে রাজ্য যোগ করুন:

lib/title_screen/title_screen.dart

import 'package:flutter/material.dart';

import '../assets.dart';
import '../styles.dart';
import 'title_screen_ui.dart';

class TitleScreen extends StatefulWidget {
  const TitleScreen({super.key});

  @override
  State<TitleScreen> createState() => _TitleScreenState();
}

class _TitleScreenState extends State<TitleScreen> {
  Color get _emitColor =>
      AppColors.emitColors[_difficultyOverride ?? _difficulty];
  Color get _orbColor =>
      AppColors.orbColors[_difficultyOverride ?? _difficulty];

  /// Currently selected difficulty
  int _difficulty = 0;

  /// Currently focused difficulty (if any)
  int? _difficultyOverride;

  void _handleDifficultyPressed(int value) {
    setState(() => _difficulty = value);
  }

  void _handleDifficultyFocused(int? value) {
    setState(() => _difficultyOverride = value);
  }

  final _finalReceiveLightAmt = 0.7;
  final _finalEmitLightAmt = 0.5;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Stack(
          children: [
            /// Bg-Base
            Image.asset(AssetPaths.titleBgBase),

            /// Bg-Receive
            _LitImage(
              color: _orbColor,
              imgSrc: AssetPaths.titleBgReceive,
              lightAmt: _finalReceiveLightAmt,
            ),

            /// Mg-Base
            _LitImage(
              imgSrc: AssetPaths.titleMgBase,
              color: _orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),

            /// Mg-Receive
            _LitImage(
              imgSrc: AssetPaths.titleMgReceive,
              color: _orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),

            /// Mg-Emit
            _LitImage(
              imgSrc: AssetPaths.titleMgEmit,
              color: _emitColor,
              lightAmt: _finalEmitLightAmt,
            ),

            /// Fg-Rocks
            Image.asset(AssetPaths.titleFgBase),

            /// Fg-Receive
            _LitImage(
              imgSrc: AssetPaths.titleFgReceive,
              color: _orbColor,
              lightAmt: _finalReceiveLightAmt,
            ),

            /// Fg-Emit
            _LitImage(
              imgSrc: AssetPaths.titleFgEmit,
              color: _emitColor,
              lightAmt: _finalEmitLightAmt,
            ),

            /// UI
            Positioned.fill(
              child: TitleScreenUi(
                difficulty: _difficulty,
                onDifficultyFocused: _handleDifficultyFocused,
                onDifficultyPressed: _handleDifficultyPressed,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _LitImage extends StatelessWidget {
  const _LitImage({
    required this.color,
    required this.imgSrc,
    required this.lightAmt,
  });
  final Color color;
  final String imgSrc;
  final double lightAmt;

  @override
  Widget build(BuildContext context) {
    final hsl = HSLColor.fromColor(color);
    return Image.asset(
      imgSrc,
      color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
      colorBlendMode: BlendMode.modulate,
    );
  }
}

এখানে দুটি ভিন্ন অসুবিধা সেটিংসে UI আছে। লক্ষ্য করুন যে গ্রেস্কেল চিত্রগুলিতে মুখোশ হিসাবে প্রয়োগ করা কঠিন রঙগুলি একটি বাস্তবসম্মত, প্রতিফলিত প্রভাব তৈরি করে!

সাধারণ অসুবিধা সহ কোডল্যাব অ্যাপটি নির্বাচিত হয়েছে, চিত্রের সম্পদগুলি বেগুনি এবং সায়ান রঙের দেখাচ্ছে৷

হার্ডকোর অসুবিধা সহ কোডল্যাব অ্যাপটি নির্বাচিত হয়েছে, ছবির সম্পদগুলি জ্বলন্ত কমলা রঙে দেখা যাচ্ছে।

স্টার্ট বোতাম যোগ করুন

  1. title_screen_ui.dart ফাইলটি আপডেট করুন। TitleScreenUi উইজেটে, নিম্নলিখিত যোগ করুন:

lib/title_screen/title_screen_ui.dart

class TitleScreenUi extends StatelessWidget {
  const TitleScreenUi({
    super.key,
    required this.difficulty,
    required this.onDifficultyPressed,
    required this.onDifficultyFocused,
  });

  final int difficulty;
  final void Function(int difficulty) onDifficultyPressed;
  final void Function(int? difficulty) onDifficultyFocused;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
      child: Stack(
        children: [
          /// Title Text
          const TopLeft(
            child: UiScaler(
              alignment: Alignment.topLeft,
              child: _TitleText(),
            ),
          ),

          /// Difficulty Btns
          BottomLeft(
            child: UiScaler(
              alignment: Alignment.bottomLeft,
              child: _DifficultyBtns(
                difficulty: difficulty,
                onDifficultyPressed: onDifficultyPressed,
                onDifficultyFocused: onDifficultyFocused,
              ),
            ),
          ),

          /// StartBtn
          BottomRight(                                    // Add from here...
            child: UiScaler(
              alignment: Alignment.bottomRight,
              child: Padding(
                padding: const EdgeInsets.only(bottom: 20, right: 40),
                child: _StartBtn(onPressed: () {}),
              ),
            ),
          ),                                              // to here.
        ],
      ),
    );
  }
}
  1. স্টার্ট বোতামটি বাস্তবায়ন করতে নিম্নলিখিত উইজেটটি যুক্ত করুন:

lib/title_screen/title_screen_ui.dart

class _StartBtn extends StatefulWidget {
  const _StartBtn({required this.onPressed});
  final VoidCallback onPressed;

  @override
  State<_StartBtn> createState() => _StartBtnState();
}

class _StartBtnState extends State<_StartBtn> {
  AnimationController? _btnAnim;
  bool _wasHovered = false;

  @override
  Widget build(BuildContext context) {
    return FocusableControlBuilder(
      cursor: SystemMouseCursors.click,
      onPressed: widget.onPressed,
      builder: (_, state) {
        if ((state.isHovered || state.isFocused) &&
            !_wasHovered &&
            _btnAnim?.status != AnimationStatus.forward) {
          _btnAnim?.forward(from: 0);
        }
        _wasHovered = (state.isHovered || state.isFocused);
        return SizedBox(
          width: 520,
          height: 100,
          child: Stack(
            children: [
              Positioned.fill(child: Image.asset(AssetPaths.titleStartBtn)),
              if (state.isHovered || state.isFocused) ...[
                Positioned.fill(
                    child: Image.asset(AssetPaths.titleStartBtnHover)),
              ],
              Center(
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: [
                    Text('START MISSION',
                        style: TextStyles.btn
                            .copyWith(fontSize: 24, letterSpacing: 18)),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

এবং এখানে বোতামগুলির একটি সম্পূর্ণ সংগ্রহের সাথে অ্যাপটি চলছে।

কোডল্যাব অ্যাপটি সাধারণ অসুবিধা সহ নির্বাচিত, শিরোনাম, অসুবিধা বোতাম এবং স্টার্ট বোতাম দেখাচ্ছে।

5. অ্যানিমেশন যোগ করুন

এই ধাপে আপনি ইউজার ইন্টারফেস এবং শিল্প সম্পদের জন্য রঙ পরিবর্তনগুলিকে অ্যানিমেট করেন।

শিরোনামে বিবর্ণ

এই ধাপে, আপনি একটি Flutter অ্যাপ অ্যানিমেট করতে একাধিক পদ্ধতি ব্যবহার করেন। পন্থাগুলির মধ্যে একটি হল flutter_animate ব্যবহার করা। এই প্যাকেজ দ্বারা চালিত অ্যানিমেশনগুলি যখনই আপনি বিকাশের পুনরাবৃত্তির গতি বাড়ানোর জন্য আপনার অ্যাপটি পুনরায় লোড করবেন তখনই স্বয়ংক্রিয়ভাবে পুনরায় প্লে হতে পারে।

  1. lib/main.dart এ কোড পরিবর্তন করুন, নিম্নরূপ:

lib/main.dart

import 'dart:io' show Platform;

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';   // Add this import
import 'package:window_size/window_size.dart';

import 'title_screen/title_screen.dart';

void main() {
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
    WidgetsFlutterBinding.ensureInitialized();
    setWindowMinSize(const Size(800, 500));
  }
  Animate.restartOnHotReload = true;                     // Add this line
  runApp(const NextGenApp());
}

class NextGenApp extends StatelessWidget {
  const NextGenApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      themeMode: ThemeMode.dark,
      darkTheme: ThemeData(brightness: Brightness.dark),
      home: const TitleScreen(),
    );
  }
}
  1. flutter_animate প্যাকেজের সুবিধা নিতে, আপনাকে অবশ্যই এটি আমদানি করতে হবে। lib/title_screen/title_screen_ui.dart এ আমদানি যোগ করুন, নিম্নরূপ:

lib/title_screen/title_screen_ui.dart

import 'package:extra_alignments/extra_alignments.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';   // Add this import
import 'package:focusable_control_builder/focusable_control_builder.dart';
import 'package:gap/gap.dart';

import '../assets.dart';
import '../common/ui_scaler.dart';
import '../styles.dart';

class TitleScreenUi extends StatelessWidget {
  1. _TitleText উইজেট সম্পাদনা করে শিরোনামে অ্যানিমেশন যোগ করুন, নিম্নরূপ:

lib/title_screen/title_screen_ui.dart

class _TitleText extends StatelessWidget {
  const _TitleText();

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Gap(20),
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Transform.translate(
              offset: Offset(-(TextStyles.h1.letterSpacing! * .5), 0),
              child: Text('OUTPOST', style: TextStyles.h1),
            ),
            Image.asset(AssetPaths.titleSelectedLeft, height: 65),
            Text('57', style: TextStyles.h2),
            Image.asset(AssetPaths.titleSelectedRight, height: 65),
          ],                                             // Edit from here...
        ).animate().fadeIn(delay: .8.seconds, duration: .7.seconds),
        Text('INTO THE UNKNOWN', style: TextStyles.h3)
            .animate()
            .fadeIn(delay: 1.seconds, duration: .7.seconds),
      ],                                                 // to here.
    );
  }
}
  1. শিরোনাম বিবর্ণ দেখতে রিলোড টিপুন।

অসুবিধা বোতাম মধ্যে বিবর্ণ

  1. _DifficultyBtns উইজেট সম্পাদনা করে অসুবিধা বোতামের প্রাথমিক উপস্থিতিতে অ্যানিমেশন যোগ করুন, নিম্নরূপ:

lib/title_screen/title_screen_ui.dart

class _DifficultyBtns extends StatelessWidget {
  const _DifficultyBtns({
    required this.difficulty,
    required this.onDifficultyPressed,
    required this.onDifficultyFocused,
  });

  final int difficulty;
  final void Function(int difficulty) onDifficultyPressed;
  final void Function(int? difficulty) onDifficultyFocused;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        _DifficultyBtn(
          label: 'Casual',
          selected: difficulty == 0,
          onPressed: () => onDifficultyPressed(0),
          onHover: (over) => onDifficultyFocused(over ? 0 : null),
        )                                                // Add from here...
            .animate()
            .fadeIn(delay: 1.3.seconds, duration: .35.seconds)
            .slide(begin: const Offset(0, .2)),          // to here
        _DifficultyBtn(
          label: 'Normal',
          selected: difficulty == 1,
          onPressed: () => onDifficultyPressed(1),
          onHover: (over) => onDifficultyFocused(over ? 1 : null),
        )                                                // Add from here...
            .animate()
            .fadeIn(delay: 1.5.seconds, duration: .35.seconds)
            .slide(begin: const Offset(0, .2)),          // to here
        _DifficultyBtn(
          label: 'Hardcore',
          selected: difficulty == 2,
          onPressed: () => onDifficultyPressed(2),
          onHover: (over) => onDifficultyFocused(over ? 2 : null),
        )                                                // Add from here...
            .animate()
            .fadeIn(delay: 1.7.seconds, duration: .35.seconds)
            .slide(begin: const Offset(0, .2)),          // to here
        const Gap(20),
      ],
    );
  }
}
  1. একটি বোনাস হিসাবে একটি সূক্ষ্ম স্লাইড আপ সহ অসুবিধা বোতামগুলি ক্রমানুসারে প্রদর্শিত দেখতে রিলোড টিপুন৷

স্টার্ট বোতামে বিবর্ণ

  1. _StartBtnState স্টেট ক্লাস সম্পাদনা করে স্টার্ট বোতামে অ্যানিমেশন যোগ করুন, নিম্নরূপ:

lib/title_screen/title_screen_ui.dart

class _StartBtnState extends State<_StartBtn> {
  AnimationController? _btnAnim;
  bool _wasHovered = false;

  @override
  Widget build(BuildContext context) {
    return FocusableControlBuilder(
      cursor: SystemMouseCursors.click,
      onPressed: widget.onPressed,
      builder: (_, state) {
        if ((state.isHovered || state.isFocused) &&
            !_wasHovered &&
            _btnAnim?.status != AnimationStatus.forward) {
          _btnAnim?.forward(from: 0);
        }
        _wasHovered = (state.isHovered || state.isFocused);
        return SizedBox(
          width: 520,
          height: 100,
          child: Stack(
            children: [
              Positioned.fill(child: Image.asset(AssetPaths.titleStartBtn)),
              if (state.isHovered || state.isFocused) ...[
                Positioned.fill(
                    child: Image.asset(AssetPaths.titleStartBtnHover)),
              ],
              Center(
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: [
                    Text('START MISSION',
                        style: TextStyles.btn
                            .copyWith(fontSize: 24, letterSpacing: 18)),
                  ],
                ),
              ),
            ],
          )                                              // Edit from here...
              .animate(autoPlay: false, onInit: (c) => _btnAnim = c)
              .shimmer(duration: .7.seconds, color: Colors.black),
        )
            .animate()
            .fadeIn(delay: 2.3.seconds)
            .slide(begin: const Offset(0, .2));
      },                                                 // to here.
    );
  }
}
  1. একটি বোনাস হিসাবে একটি সূক্ষ্ম স্লাইড আপ সহ অসুবিধা বোতামগুলি ক্রমানুসারে প্রদর্শিত দেখতে রিলোড টিপুন৷

অসুবিধা হোভার প্রভাব অ্যানিমেট

_DifficultyBtn স্টেট ক্লাস সম্পাদনা করে অসুবিধা বোতামের হোভার স্টেটে অ্যানিমেশন যোগ করুন, নিম্নরূপ:

lib/title_screen/title_screen_ui.dart

class _DifficultyBtn extends StatelessWidget {
  const _DifficultyBtn({
    required this.selected,
    required this.onPressed,
    required this.onHover,
    required this.label,
  });
  final String label;
  final bool selected;
  final VoidCallback onPressed;
  final void Function(bool hasFocus) onHover;

  @override
  Widget build(BuildContext context) {
    return FocusableControlBuilder(
      onPressed: onPressed,
      onHoverChanged: (_, state) => onHover.call(state.isHovered),
      builder: (_, state) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: SizedBox(
            width: 250,
            height: 60,
            child: Stack(
              children: [
                /// Bg with fill and outline
                AnimatedOpacity(                         // Edit from here
                  opacity: (!selected && (state.isHovered || state.isFocused))
                      ? 1
                      : 0,
                  duration: .3.seconds,
                  child: Container(
                    decoration: BoxDecoration(
                      color: const Color(0xFF00D1FF).withOpacity(.1),
                      border: Border.all(color: Colors.white, width: 5),
                    ),
                  ),
                ),                                       // to here.

                if (state.isHovered || state.isFocused) ...[
                  Container(
                    decoration: BoxDecoration(
                      color: const Color(0xFF00D1FF).withOpacity(.1),
                    ),
                  ),
                ],

                /// cross-hairs (selected state)
                if (selected) ...[
                  CenterLeft(
                    child: Image.asset(AssetPaths.titleSelectedLeft),
                  ),
                  CenterRight(
                    child: Image.asset(AssetPaths.titleSelectedRight),
                  ),
                ],

                /// Label
                Center(
                  child: Text(label.toUpperCase(), style: TextStyles.btn),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

অসুবিধা বোতামগুলি এখন BoxDecoration দেখায় যখন মাউস একটি বোতামের উপর ঘোরায় যা নির্বাচন করা হয়নি।

রঙ পরিবর্তন অ্যানিমেট

  1. পটভূমির রঙ পরিবর্তন তাত্ক্ষণিক এবং কঠোর। রঙের স্কিমগুলির মধ্যে আলোকিত চিত্রগুলিকে অ্যানিমেট করা ভাল। lib/title_screen/title_screen.dartflutter_animate যোগ করুন :

lib/title_screen/title_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';    // Add this import

import '../assets.dart';
import '../styles.dart';
import 'title_screen_ui.dart';

class TitleScreen extends StatefulWidget {
  1. lib/title_screen/title_screen.dart এ একটি _AnimatedColors উইজেট যোগ করুন :

lib/title_screen/title_screen.dart

class _AnimatedColors extends StatelessWidget {
  const _AnimatedColors({
    required this.emitColor,
    required this.orbColor,
    required this.builder,
  });

  final Color emitColor;
  final Color orbColor;

  final Widget Function(BuildContext context, Color orbColor, Color emitColor)
      builder;

  @override
  Widget build(BuildContext context) {
    final duration = .5.seconds;
    return TweenAnimationBuilder(
      tween: ColorTween(begin: emitColor, end: emitColor),
      duration: duration,
      builder: (_, emitColor, __) {
        return TweenAnimationBuilder(
          tween: ColorTween(begin: orbColor, end: orbColor),
          duration: duration,
          builder: (context, orbColor, __) {
            return builder(context, orbColor!, emitColor!);
          },
        );
      },
    );
  }
}
  1. _TitleScreenStatebuild মেথড আপডেট করে আলোকিত ইমেজগুলোর রং অ্যানিমেট করার জন্য আপনি যে উইজেটটি তৈরি করেছেন তা ব্যবহার করুন:

lib/title_screen/title_screen.dart

class _TitleScreenState extends State<TitleScreen> {
  Color get _emitColor =>
      AppColors.emitColors[_difficultyOverride ?? _difficulty];
  Color get _orbColor =>
      AppColors.orbColors[_difficultyOverride ?? _difficulty];

  /// Currently selected difficulty
  int _difficulty = 0;

  /// Currently focused difficulty (if any)
  int? _difficultyOverride;

  void _handleDifficultyPressed(int value) {
    setState(() => _difficulty = value);
  }

  void _handleDifficultyFocused(int? value) {
    setState(() => _difficultyOverride = value);
  }

  final _finalReceiveLightAmt = 0.7;
  final _finalEmitLightAmt = 0.5;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: _AnimatedColors(                           // Edit from here...
          orbColor: _orbColor,
          emitColor: _emitColor,
          builder: (_, orbColor, emitColor) {
            return Stack(
              children: [
                /// Bg-Base
                Image.asset(AssetPaths.titleBgBase),

                /// Bg-Receive
                _LitImage(
                  color: orbColor,
                  imgSrc: AssetPaths.titleBgReceive,
                  lightAmt: _finalReceiveLightAmt,
                ),

                /// Mg-Base
                _LitImage(
                  imgSrc: AssetPaths.titleMgBase,
                  color: orbColor,
                  lightAmt: _finalReceiveLightAmt,
                ),

                /// Mg-Receive
                _LitImage(
                  imgSrc: AssetPaths.titleMgReceive,
                  color: orbColor,
                  lightAmt: _finalReceiveLightAmt,
                ),

                /// Mg-Emit
                _LitImage(
                  imgSrc: AssetPaths.titleMgEmit,
                  color: emitColor,
                  lightAmt: _finalEmitLightAmt,
                ),

                /// Fg-Rocks
                Image.asset(AssetPaths.titleFgBase),

                /// Fg-Receive
                _LitImage(
                  imgSrc: AssetPaths.titleFgReceive,
                  color: orbColor,
                  lightAmt: _finalReceiveLightAmt,
                ),

                /// Fg-Emit
                _LitImage(
                  imgSrc: AssetPaths.titleFgEmit,
                  color: emitColor,
                  lightAmt: _finalEmitLightAmt,
                ),

                /// UI
                Positioned.fill(
                  child: TitleScreenUi(
                    difficulty: _difficulty,
                    onDifficultyFocused: _handleDifficultyFocused,
                    onDifficultyPressed: _handleDifficultyPressed,
                  ),
                ),
              ],
            ).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
          },
        ),                                                // to here.
      ),
    );
  }
}

এই চূড়ান্ত সম্পাদনার মাধ্যমে, আপনি স্ক্রিনের প্রতিটি উপাদানে অ্যানিমেশন যোগ করেছেন এবং এটি দেখতে অনেক বেশি ভালো লাগছে!

6. ফ্র্যাগমেন্ট শেডার যোগ করুন

এই ধাপে আপনি অ্যাপে ফ্র্যাগমেন্ট শেডার যোগ করুন। প্রথমত, আপনি শিরোনামটিকে আরও ডিস্টোপিয়ান অনুভূতি দেওয়ার জন্য একটি শেডার ব্যবহার করেন। তারপর, আপনি একটি অরব তৈরি করতে একটি দ্বিতীয় শেডার যোগ করুন যা পৃষ্ঠার কেন্দ্রীয় কেন্দ্রবিন্দু হিসাবে কাজ করে।

একটি খণ্ড শেডার দিয়ে শিরোনাম বিকৃত করা

এই পরিবর্তনের সাথে আপনি provider প্যাকেজ প্রবর্তন করেন, যা উইজেট গাছের নিচে কম্পাইল করা শেডার্স পাস করতে সক্ষম করে। আপনি যদি শেডারগুলি কীভাবে লোড করা হয় তাতে আগ্রহী হন, lib/assets.dart এ বাস্তবায়ন দেখুন।

  1. lib/main.dart এ কোড পরিবর্তন করুন, নিম্নরূপ:

lib/main.dart

import 'dart:io' show Platform;

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:provider/provider.dart';                 // Add this import
import 'package:window_size/window_size.dart';

import 'assets.dart';                                    // Add this import
import 'title_screen/title_screen.dart';

void main() {
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
    WidgetsFlutterBinding.ensureInitialized();
    setWindowMinSize(const Size(800, 500));
  }
  Animate.restartOnHotReload = true;
  runApp(                                                // Edit from here...
    FutureProvider<FragmentPrograms?>(
      create: (context) => loadFragmentPrograms(),
      initialData: null,
      child: const NextGenApp(),
    ),
  );                                                     // to here.
}

class NextGenApp extends StatelessWidget {
  const NextGenApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      themeMode: ThemeMode.dark,
      darkTheme: ThemeData(brightness: Brightness.dark),
      home: const TitleScreen(),
    );
  }
}
  1. provider প্যাকেজ, এবং step_01 এ অন্তর্ভুক্ত শেডার ইউটিলিটিগুলির সুবিধা নিতে, আপনাকে সেগুলি আমদানি করতে হবে৷ lib/title_screen/title_screen_ui.dart এ নতুন আমদানি যোগ করুন, নিম্নরূপ:

lib/title_screen/title_screen_ui.dart

import 'package:extra_alignments/extra_alignments.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:focusable_control_builder/focusable_control_builder.dart';
import 'package:gap/gap.dart';
import 'package:provider/provider.dart';                 // Add this import

import '../assets.dart';
import '../common/shader_effect.dart';                   // And this import
import '../common/ticking_builder.dart';                 // And this import
import '../common/ui_scaler.dart';
import '../styles.dart';

class TitleScreenUi extends StatelessWidget {
  1. নিচের মতো _TitleText উইজেট সম্পাদনা করে শেডার দিয়ে শিরোনামটি বিকৃত করুন:

lib/title_screen/title_screen_ui.dart

class _TitleText extends StatelessWidget {
  const _TitleText();

  @override
  Widget build(BuildContext context) {
    Widget content = Column(                             // Modify this line
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Gap(20),
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Transform.translate(
              offset: Offset(-(TextStyles.h1.letterSpacing! * .5), 0),
              child: Text('OUTPOST', style: TextStyles.h1),
            ),
            Image.asset(AssetPaths.titleSelectedLeft, height: 65),
            Text('57', style: TextStyles.h2),
            Image.asset(AssetPaths.titleSelectedRight, height: 65),
          ],
        ).animate().fadeIn(delay: .8.seconds, duration: .7.seconds),
        Text('INTO THE UNKNOWN', style: TextStyles.h3)
            .animate()
            .fadeIn(delay: 1.seconds, duration: .7.seconds),
      ],
    );
    return Consumer<FragmentPrograms?>(                  // Add from here...
      builder: (context, fragmentPrograms, _) {
        if (fragmentPrograms == null) return content;
        return TickingBuilder(
          builder: (context, time) {
            return AnimatedSampler(
              (image, size, canvas) {
                const double overdrawPx = 30;
                final shader = fragmentPrograms.ui.fragmentShader();
                shader
                  ..setFloat(0, size.width)
                  ..setFloat(1, size.height)
                  ..setFloat(2, time)
                  ..setImageSampler(0, image);
                Rect rect = Rect.fromLTWH(-overdrawPx, -overdrawPx,
                    size.width + overdrawPx, size.height + overdrawPx);
                canvas.drawRect(rect, Paint()..shader = shader);
              },
              child: content,
            );
          },
        );
      },
    );                                                   // to here.
  }
}

আপনি শিরোনাম বিকৃত দেখতে পাবেন-যেমন আপনি একটি dystopian ভবিষ্যতে আশা করতে পারেন.

কক্ষ যোগ করুন

এখন জানালার কেন্দ্রে কক্ষ যোগ করুন। আপনাকে স্টার্ট বোতামে একটি onPressed কলব্যাক যোগ করতে হবে।

  1. lib/title_screen/title_screen_ui.dart এ, TitleScreenUi নিম্নরূপ পরিবর্তন করুন:

lib/title_screen/title_screen_ui.dart

class TitleScreenUi extends StatelessWidget {
  const TitleScreenUi({
    super.key,
    required this.difficulty,
    required this.onDifficultyPressed,
    required this.onDifficultyFocused,
    required this.onStartPressed,                         // Add this argument
  });

  final int difficulty;
  final void Function(int difficulty) onDifficultyPressed;
  final void Function(int? difficulty) onDifficultyFocused;
  final VoidCallback onStartPressed;                      // Add this attribute

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
      child: Stack(
        children: [
          /// Title Text
          const TopLeft(
            child: UiScaler(
              alignment: Alignment.topLeft,
              child: _TitleText(),
            ),
          ),

          /// Difficulty Btns
          BottomLeft(
            child: UiScaler(
              alignment: Alignment.bottomLeft,
              child: _DifficultyBtns(
                difficulty: difficulty,
                onDifficultyPressed: onDifficultyPressed,
                onDifficultyFocused: onDifficultyFocused,
              ),
            ),
          ),

          /// StartBtn
          BottomRight(
            child: UiScaler(
              alignment: Alignment.bottomRight,
              child: Padding(
                padding: const EdgeInsets.only(bottom: 20, right: 40),
                child: _StartBtn(onPressed: onStartPressed),  // Edit this line
              ),
            ),
          ),
        ],
      ),
    );
  }
}

এখন আপনি একটি কলব্যাক সহ স্টার্ট বোতামটি পরিবর্তন করেছেন, আপনাকে lib/title_screen/title_screen.dart ফাইলে ব্যাপক পরিবর্তন করতে হবে।

  1. নিম্নরূপ আমদানি পরিবর্তন করুন:

lib/title_screen/title_screen.dart

import 'dart:math';                                       // Add this import
import 'dart:ui';                                         // And this import

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';                   // Add this import
import 'package:flutter_animate/flutter_animate.dart';

import '../assets.dart';
import '../orb_shader/orb_shader_config.dart';            // And this import
import '../orb_shader/orb_shader_widget.dart';            // And this import too
import '../styles.dart';
import 'title_screen_ui.dart';

class TitleScreen extends StatefulWidget {
  1. নিচের সাথে মেলে _TitleScreenState পরিবর্তন করুন। ক্লাসের প্রায় প্রতিটি অংশই কোনো না কোনোভাবে পরিবর্তিত হয়।

lib/title_screen/title_screen.dart

class _TitleScreenState extends State<TitleScreen>
    with SingleTickerProviderStateMixin {
  final _orbKey = GlobalKey<OrbShaderWidgetState>();

  /// Editable Settings
  /// 0-1, receive lighting strength
  final _minReceiveLightAmt = .35;
  final _maxReceiveLightAmt = .7;

  /// 0-1, emit lighting strength
  final _minEmitLightAmt = .5;
  final _maxEmitLightAmt = 1;

  /// Internal
  var _mousePos = Offset.zero;

  Color get _emitColor =>
      AppColors.emitColors[_difficultyOverride ?? _difficulty];
  Color get _orbColor =>
      AppColors.orbColors[_difficultyOverride ?? _difficulty];

  /// Currently selected difficulty
  int _difficulty = 0;

  /// Currently focused difficulty (if any)
  int? _difficultyOverride;
  double _orbEnergy = 0;
  double _minOrbEnergy = 0;

  double get _finalReceiveLightAmt {
    final light =
        lerpDouble(_minReceiveLightAmt, _maxReceiveLightAmt, _orbEnergy) ?? 0;
    return light + _pulseEffect.value * .05 * _orbEnergy;
  }

  double get _finalEmitLightAmt {
    return lerpDouble(_minEmitLightAmt, _maxEmitLightAmt, _orbEnergy) ?? 0;
  }

  late final _pulseEffect = AnimationController(
    vsync: this,
    duration: _getRndPulseDuration(),
    lowerBound: -1,
    upperBound: 1,
  );

  Duration _getRndPulseDuration() => 100.ms + 200.ms * Random().nextDouble();

  double _getMinEnergyForDifficulty(int difficulty) => switch (difficulty) {
        1 => 0.3,
        2 => 0.6,
        _ => 0,
      };


  @override
  void initState() {
    super.initState();
    _pulseEffect.forward();
    _pulseEffect.addListener(_handlePulseEffectUpdate);
  }

  void _handlePulseEffectUpdate() {
    if (_pulseEffect.status == AnimationStatus.completed) {
      _pulseEffect.reverse();
      _pulseEffect.duration = _getRndPulseDuration();
    } else if (_pulseEffect.status == AnimationStatus.dismissed) {
      _pulseEffect.duration = _getRndPulseDuration();
      _pulseEffect.forward();
    }
  }

  void _handleDifficultyPressed(int value) {
    setState(() => _difficulty = value);
    _bumpMinEnergy();
  }

  Future<void> _bumpMinEnergy([double amount = 0.1]) async {
    setState(() {
      _minOrbEnergy = _getMinEnergyForDifficulty(_difficulty) + amount;
    });
    await Future<void>.delayed(.2.seconds);
    setState(() {
      _minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
    });
  }

  void _handleStartPressed() => _bumpMinEnergy(0.3);

  void _handleDifficultyFocused(int? value) {
    setState(() {
      _difficultyOverride = value;
      if (value == null) {
        _minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
      } else {
        _minOrbEnergy = _getMinEnergyForDifficulty(value);
      }
    });
  }

  /// Update mouse position so the orbWidget can use it, doing it here prevents
  /// btns from blocking the mouse-move events in the widget itself.
  void _handleMouseMove(PointerHoverEvent e) {
    setState(() {
      _mousePos = e.localPosition;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: MouseRegion(
          onHover: _handleMouseMove,
          child: _AnimatedColors(
            orbColor: _orbColor,
            emitColor: _emitColor,
            builder: (_, orbColor, emitColor) {
              return Stack(
                children: [
                  /// Bg-Base
                  Image.asset(AssetPaths.titleBgBase),

                  /// Bg-Receive
                  _LitImage(
                    color: orbColor,
                    imgSrc: AssetPaths.titleBgReceive,
                    pulseEffect: _pulseEffect,
                    lightAmt: _finalReceiveLightAmt,
                  ),

                  /// Orb
                  Positioned.fill(
                    child: Stack(
                      children: [
                        // Orb
                        OrbShaderWidget(
                          key: _orbKey,
                          mousePos: _mousePos,
                          minEnergy: _minOrbEnergy,
                          config: OrbShaderConfig(
                            ambientLightColor: orbColor,
                            materialColor: orbColor,
                            lightColor: orbColor,
                          ),
                          onUpdate: (energy) => setState(() {
                            _orbEnergy = energy;
                          }),
                        ),
                      ],
                    ),
                  ),

                  /// Mg-Base
                  _LitImage(
                    imgSrc: AssetPaths.titleMgBase,
                    color: orbColor,
                    pulseEffect: _pulseEffect,
                    lightAmt: _finalReceiveLightAmt,
                  ),

                  /// Mg-Receive
                  _LitImage(
                    imgSrc: AssetPaths.titleMgReceive,
                    color: orbColor,
                    pulseEffect: _pulseEffect,
                    lightAmt: _finalReceiveLightAmt,
                  ),

                  /// Mg-Emit
                  _LitImage(
                    imgSrc: AssetPaths.titleMgEmit,
                    color: emitColor,
                    pulseEffect: _pulseEffect,
                    lightAmt: _finalEmitLightAmt,
                  ),

                  /// Fg-Rocks
                  Image.asset(AssetPaths.titleFgBase),

                  /// Fg-Receive
                  _LitImage(
                    imgSrc: AssetPaths.titleFgReceive,
                    color: orbColor,
                    pulseEffect: _pulseEffect,
                    lightAmt: _finalReceiveLightAmt,
                  ),

                  /// Fg-Emit
                  _LitImage(
                    imgSrc: AssetPaths.titleFgEmit,
                    color: emitColor,
                    pulseEffect: _pulseEffect,
                    lightAmt: _finalEmitLightAmt,
                  ),

                  /// UI
                  Positioned.fill(
                    child: TitleScreenUi(
                      difficulty: _difficulty,
                      onDifficultyFocused: _handleDifficultyFocused,
                      onDifficultyPressed: _handleDifficultyPressed,
                      onStartPressed: _handleStartPressed,
                    ),
                  ),
                ],
              ).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
            },
          ),
        ),
      ),
    );
  }
}
  1. নিম্নরূপ _LitImage সংশোধন করুন:

lib/title_screen/title_screen.dart

class _LitImage extends StatelessWidget {
  const _LitImage({
    required this.color,
    required this.imgSrc,
    required this.pulseEffect,                            // Add this parameter
    required this.lightAmt,
  });
  final Color color;
  final String imgSrc;
  final AnimationController pulseEffect;                  // Add this attribute
  final double lightAmt;

  @override
  Widget build(BuildContext context) {
    final hsl = HSLColor.fromColor(color);
    return ListenableBuilder(                             // Edit from here...
      listenable: pulseEffect,
      builder: (context, child) {
        return Image.asset(
          imgSrc,
          color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
          colorBlendMode: BlendMode.modulate,
        );
      },
    );                                                    // to here.
  }
}

এই সংযোজনের ফল।

7. কণা অ্যানিমেশন যোগ করুন

এই ধাপে, আপনি অ্যাপে একটি সূক্ষ্ম স্পন্দন আন্দোলন তৈরি করতে কণা অ্যানিমেশন যোগ করুন।

সব জায়গায় কণা যোগ করুন

  1. একটি নতুন lib/title_screen/particle_overlay.dart ফাইল তৈরি করুন এবং তারপরে নিম্নলিখিত কোড যোগ করুন:

lib/title_screen/particle_overlay.dart

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:particle_field/particle_field.dart';
import 'package:rnd/rnd.dart';

class ParticleOverlay extends StatelessWidget {
  const ParticleOverlay({super.key, required this.color, required this.energy});

  final Color color;
  final double energy;

  @override
  Widget build(BuildContext context) {
    return ParticleField(
      spriteSheet: SpriteSheet(
        image: const AssetImage('assets/images/particle-wave.png'),
      ),
      // blend the image's alpha with the specified color:
      blendMode: BlendMode.dstIn,

      // this runs every tick:
      onTick: (controller, _, size) {
        List<Particle> particles = controller.particles;

        // add a new particle with random angle, distance & velocity:
        double a = rnd(pi * 2);
        double dist = rnd(1, 4) * 35 + 150 * energy;
        double vel = rnd(1, 2) * (1 + energy * 1.8);
        particles.add(Particle(
          // how many ticks this particle will live:
          lifespan: rnd(1, 2) * 20 + energy * 15,
          // starting distance from center:
          x: cos(a) * dist,
          y: sin(a) * dist,
          // starting velocity:
          vx: cos(a) * vel,
          vy: sin(a) * vel,
          // other starting values:
          rotation: a,
          scale: rnd(1, 2) * 0.6 + energy * 0.5,
        ));

        // update all of the particles:
        for (int i = particles.length - 1; i >= 0; i--) {
          Particle p = particles[i];
          if (p.lifespan <= 0) {
            // particle is expired, remove it:
            particles.removeAt(i);
            continue;
          }
          p.update(
            scale: p.scale * 1.025,
            vx: p.vx * 1.025,
            vy: p.vy * 1.025,
            color: color.withOpacity(p.lifespan * 0.001 + 0.01),
            lifespan: p.lifespan - 1,
          );
        }
      },
    );
  }
}
  1. lib/title_screen/title_screen.dart এর জন্য আমদানি পরিবর্তন করুন, নিম্নরূপ:

lib/title_screen/title_screen.dart

import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';

import '../assets.dart';
import '../orb_shader/orb_shader_config.dart';
import '../orb_shader/orb_shader_widget.dart';
import '../styles.dart';
import 'particle_overlay.dart';                          // Add this import
import 'title_screen_ui.dart';

class TitleScreen extends StatefulWidget {
  1. _TitleScreenState এর build পদ্ধতি পরিবর্তন করে UI-তে ParticleOverlay যোগ করুন, নিম্নরূপ:

lib/title_screen/title_screen.dart

@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.black,
    body: Center(
      child: MouseRegion(
        onHover: _handleMouseMove,
        child: _AnimatedColors(
          orbColor: _orbColor,
          emitColor: _emitColor,
          builder: (_, orbColor, emitColor) {
            return Stack(
              children: [
                /// Bg-Base
                Image.asset(AssetPaths.titleBgBase),

                /// Bg-Receive
                _LitImage(
                  color: orbColor,
                  imgSrc: AssetPaths.titleBgReceive,
                  pulseEffect: _pulseEffect,
                  lightAmt: _finalReceiveLightAmt,
                ),

                /// Orb
                Positioned.fill(
                  child: Stack(
                    children: [
                      // Orb
                      OrbShaderWidget(
                        key: _orbKey,
                        mousePos: _mousePos,
                        minEnergy: _minOrbEnergy,
                        config: OrbShaderConfig(
                          ambientLightColor: orbColor,
                          materialColor: orbColor,
                          lightColor: orbColor,
                        ),
                        onUpdate: (energy) => setState(() {
                          _orbEnergy = energy;
                        }),
                      ),
                    ],
                  ),
                ),

                /// Mg-Base
                _LitImage(
                  imgSrc: AssetPaths.titleMgBase,
                  color: orbColor,
                  pulseEffect: _pulseEffect,
                  lightAmt: _finalReceiveLightAmt,
                ),

                /// Mg-Receive
                _LitImage(
                  imgSrc: AssetPaths.titleMgReceive,
                  color: orbColor,
                  pulseEffect: _pulseEffect,
                  lightAmt: _finalReceiveLightAmt,
                ),

                /// Mg-Emit
                _LitImage(
                  imgSrc: AssetPaths.titleMgEmit,
                  color: emitColor,
                  pulseEffect: _pulseEffect,
                  lightAmt: _finalEmitLightAmt,
                ),

                /// Particle Field
                Positioned.fill(                          // Add from here...
                  child: IgnorePointer(
                    child: ParticleOverlay(
                      color: orbColor,
                      energy: _orbEnergy,
                    ),
                  ),
                ),                                        // to here.

                /// Fg-Rocks
                Image.asset(AssetPaths.titleFgBase),

                /// Fg-Receive
                _LitImage(
                  imgSrc: AssetPaths.titleFgReceive,
                  color: orbColor,
                  pulseEffect: _pulseEffect,
                  lightAmt: _finalReceiveLightAmt,
                ),

                /// Fg-Emit
                _LitImage(
                  imgSrc: AssetPaths.titleFgEmit,
                  color: emitColor,
                  pulseEffect: _pulseEffect,
                  lightAmt: _finalEmitLightAmt,
                ),

                /// UI
                Positioned.fill(
                  child: TitleScreenUi(
                    difficulty: _difficulty,
                    onDifficultyFocused: _handleDifficultyFocused,
                    onDifficultyPressed: _handleDifficultyPressed,
                    onStartPressed: _handleStartPressed,
                  ),
                ),
              ],
            ).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
          },
        ),
      ),
    ),
  );
}

চূড়ান্ত ফলাফলে অ্যানিমেশন, ফ্র্যাগমেন্ট শেডার এবং কণা প্রভাব রয়েছে—একাধিক প্ল্যাটফর্মে!

সর্বত্র কণা যোগ করুন—এমনকি ওয়েবও

কোডের সাথে একটি সামান্য সমস্যা আছে কারণ এটি দাঁড়িয়ে আছে। যখন ফ্লাটার ওয়েবে চলে, তখন দুটি বিকল্প রেন্ডারিং ইঞ্জিন ব্যবহার করা যেতে পারে: ক্যানভাসকিট ইঞ্জিন, যা ডেস্কটপ ক্লাস ব্রাউজারগুলিতে ডিফল্টরূপে ব্যবহৃত হয় এবং একটি HTML DOM রেন্ডারার, যা মোবাইল ডিভাইসের জন্য ডিফল্টরূপে ব্যবহৃত হয়। সমস্যা হল যে HTML DOM রেন্ডারার ফ্র্যাগমেন্ট শেডার সমর্থন করে না।

সমাধান হল শুধুমাত্র ক্যানভাসকিট রেন্ডারার ব্যবহার করে ওয়েবের জন্য তৈরি করা। এটি করার জন্য, নিম্নরূপ বিল্ড কমান্ডে একটি পতাকা যুক্ত করুন:

$ flutter build web --web-renderer canvaskit
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 7692 bytes (99.5% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag
when building your app.
Font asset "CupertinoIcons.ttf" was tree-shaken, reducing it from 257628 to 1172 bytes (99.5% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when
building your app.
Compiling lib/main.dart for the Web...                             15.6s
✓ Built build/web

এখানে আপনার সমস্ত কঠোর পরিশ্রম, এই সময় একটি Chrome ব্রাউজারে দেখানো হয়েছে।

8. অভিনন্দন

আপনি অ্যানিমেশন, ফ্র্যাগমেন্ট শেডার এবং কণা অ্যানিমেশন সহ একটি সম্পূর্ণ বৈশিষ্ট্যযুক্ত গেম ইন্ট্রো স্ক্রিন তৈরি করেছেন! আপনি এখন Flutter সমর্থন করে এমন সমস্ত প্ল্যাটফর্মে এই কৌশলগুলি ব্যবহার করতে পারেন৷

আরও জানুন