Để hiển thị hiệu ứng **Nhiễu hạt (Film Grain/Noise)** trực tiếp trên kính ngắm (Viewfinder) của camera mà vẫn đảm bảo tốc độ mượt mà **60 FPS không bị lag**, chúng ta tuyệt đối không thể xử lý bằng vòng lặp pixel tuần tự trên CPU. Giải pháp tối ưu nhất là sử dụng sức mạnh đồ họa phần cứng (GPU Shader) trực tiếp ở tầng Flutter UI để phủ một lớp texture hạt động (Dynamic Grain Filter) đè lên luồng Camera Preview thô.

Dưới đây là kế hoạch Markdown chi tiết từ giải pháp giao diện hiển thị cho đến thuật toán đồng bộ áp hạt lên ảnh chụp gốc khi nhấn Shutter.

---

# 🎞️ KẾ HOẠCH TRIỂN KHAI: HIỂN THỊ NHIỄU HẠT (FILM GRAIN) REAL-TIME TRÊN VIEWFINDER

Mục tiêu cốt lõi:

1. **Live Preview trơn tru (60 FPS):** Sử dụng các hiệu ứng phần cứng đồ họa tích hợp hoặc Shader tùy biến trong Flutter để tạo lớp hạt chuyển động liên tục, phản hồi tức thì theo mức độ tăng giảm trên thanh thước đo.
2. **Dynamic Noise Generation:** Hạt nhiễu phải liên tục thay đổi cấu trúc qua từng khung hình (không phải ảnh hạt tĩnh đứng im) để tạo cảm giác nhiễu hạt phim điện ảnh tự nhiên.
3. **Đồng bộ ảnh chụp độ phân giải cao:** Áp dụng thuật toán tạo nhiễu hạt tương ứng lên Bitmap ảnh gốc dưới luồng ngầm (Background Coroutine) khi bấm chụp.

---

## 📐 1. Tích Hợp Lớp Phủ Nhiễu Hạt Động Trên Giao Diện UI (`CameraScreen.dart`)

Trong Flutter, phương pháp tối ưu nhất để tạo nhiễu hạt thực tế mà không gánh CPU là sử dụng một hàm lặp tạo hạt ngẫu nhiên thông qua widget **`CustomPainter`** kết hợp với một vòng lặp Animation kích hoạt vẽ lại (repaint) liên tục, tạo ra các hạt nhiễu chuyển động động học.

### 💻 Đoạn mã tạo hiệu ứng Texture hạt trên kính ngắm:

```dart
import 'dart:math';
import 'package:flutter/material.dart';

// 1. LỚP VẼ HẠT NHIỄU PHẦN CỨNG (GPU ACCELERATED PAINTER)
class GrainPainter extends CustomPainter {
  final double intensity; // Độ đậm/nhạt của hạt grain (0.0 -> 1.0)
  final Random _random = Random();

  GrainPainter({required this.intensity});

  @override
  void paint(Canvas canvas, Size size) {
    if (intensity <= 0) return;

    final Paint paint = Paint()..style = PaintingStyle.fill;
    
    // Tính toán mật độ hạt dựa trên thông số cấu hình. 
    // Tránh quét từng pixel, ta chọn ngẫu nhiên các điểm tọa độ để tối ưu RAM
    int grainCount = (size.width * size.height * 0.1 * intensity).toInt();

    for (int i = 0; i < grainCount; i++) {
      double x = _random.nextDouble() * size.width;
      double y = _random.nextDouble() * size.height;
      
      // Tạo sắc độ hạt ngẫu nhiên từ xám trắng đến xám tối để giả lập Grain tự nhiên
      int grayValue = _random.nextInt(55) + 100; // Dải màu hạt phim [100 - 155]
      // Độ trong suốt của từng hạt phụ thuộc vào cường độ chỉnh trên thước đo
      int alpha = (_random.nextDouble() * intensity * 120).toInt().clamp(0, 255);

      paint.color = Color.fromARGB(alpha, grayValue, grayValue, grayValue);
      
      // Vẽ hạt nhiễu kích thước siêu nhỏ từ 1 đến 1.5 pixel
      canvas.drawRect(Rect.fromLTWH(x, y, 1.2, 1.2), paint);
    }
  }

  // Luôn trả về true để ép GPU sinh ma trận hạt mới liên tục theo khung hình Animation
  @override
  bool shouldRepaint(covariant GrainPainter oldDelegate) => true;
}

// 2. TÍCH HỢP LỚP HẠT ĐỘNG VÀO VIEWFINDER
class LiveCameraGrainWidget extends StatefulWidget {
  final double grainIntensity; // Thừa hưởng từ thanh trượt cài đặt
  const LiveCameraGrainWidget({super.key, required this.grainIntensity});

  @override
  State<LiveCameraGrainWidget> createState() => _LiveCameraGrainWidgetState();
}

class _LiveCameraGrainWidgetState extends State<LiveCameraGrainWidget> with SingleTickerProviderStateMixin {
  AnimationController? _animationController;

  @override
  void initState() {
    super.initState() ;
    // Vòng lặp vô hạn chạy ở tần số quét màn hình (60Hz) để đổi vị trí hạt liên tục
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    )..repeat();
  }

  @override
  void dispose() {
    _animationController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 3 / 4,
      child: Stack(
        children: [
          // Lớp đáy: Kính ngắm camera thô (mượt mà 60 FPS)
          CameraPreview(_cameraController),

          // Lớp đỉnh: Phủ lớp texture hạt động bằng AnimatedBuilder
          AnimatedBuilder(
            animation: _animationController!,
            builder: (context, child) {
              return CustomPaint(
                size: Size.infinite,
                painter: GrainPainter(intensity: widget.grainIntensity),
              );
            },
          ),
        ],
      ),
    ),
  );
}

```

---

## 💻 2. Thuật Toán Sinh Nhiễu Hạt Lên Ảnh Gốc Khi Bấm Chụp (`MainActivity.kt`)

Khi người dùng nhấn nút Shutter, để bức ảnh lưu về máy có hiệu ứng hạt giống hệt như kính ngắm, chúng ta đưa thuật toán toán học tạo hạt phim (Film Grain Noise Model) vào luồng xử lý ngầm Coroutine để tránh đơ ứng dụng.

```kotlin
import android.graphics.Bitmap
import android.graphics.Color
import java.util.Random

fun applyHighQualityFilmGrain(inputBitmap: Bitmap, grainIntensity: Float): Bitmap {
    if (grainIntensity <= 0.0f) return inputBitmap

    val w = inputBitmap.width
    val h = inputBitmap.height
    val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
    val random = Random()

    // Khuếch đại biên độ tác động nhiễu hạt để tạo sự thay đổi rõ rệt
    val grainStrength = grainIntensity * 45.0f

    for (y in 0 until h) {
        for (x in 0 until w) {
            val pixel = inputBitmap.getPixel(x, y)
            
            val r = (pixel shr 16 and 0xFF)
            val g = (pixel shr 8 and 0xFF)
            val b = (pixel and 0xFF)

            // Sử dụng thuật toán phân phối Gauss (Gaussian Noise) để hạt mịn và chân thực hơn nhiễu thô
            val noise = (random.nextGaussian() * grainStrength).toInt()

            // Cộng hạt nhiễu trực tiếp vào từng kênh màu, khống chế dải màu trong giới hạn [0, 255]
            val finalR = (r + noise).coerceIn(0, 255)
            val finalG = (g + noise).coerceIn(0, 255)
            val finalB = (b + noise).coerceIn(0, 255)

            outputBitmap.setPixel(x, y, (0xFF shl 24) or (finalR shl 16) or (finalG shl 8) or finalB)
        }
    }
    return outputBitmap
}

```

---

## ⚡ 3. Đồng Bộ Luồng Xử Lý Bất Đồng Bộ Khi Chụp

Đảm bảo ảnh xuất ra từ camera được nạp đầy đủ thông số cấu hình nhiễu hạt hiện tại từ thanh trượt điều khiển:

```kotlin
fun onShutterPressedWithGrain(capturedBitmap: Bitmap, currentGrainIntensity: Float) {
    // 1. Kích hoạt chớp màn hình và hiển thị thumbnail lập tức
    triggerFlashEffectUI()

    // 2. Cô lập tiến trình đổ màu hạt xuống tầng luồng ngầm độc lập
    GlobalScope.launch(Dispatchers.Default) {
        // Áp dụng thuật toán gieo hạt nhiễu độ phân giải cao
        val grainPhoto = applyHighQualityFilmGrain(capturedBitmap, currentGrainIntensity)
        
        // Tiến hành ghi file vào Bộ sưu tập hệ thống
        saveToSystemGallery(grainPhoto)
        grainPhoto.recycle()
    }
}

```

---

## 📅 4. Checklist Xác Minh Bản Sửa Đổi (Verification Checklist)

* [ ] **Kiểm thử tốc độ Viewfinder (FPS Test):** Kéo thông số grain lên mức tối đa (`+100`) trên thanh thước đo. Quan sát kính ngắm camera xem luồng preview có di chuyển trơn tru, ổn định không (Yêu cầu: Khung hình không bị tụt FPS, đạt độ mượt mà phần cứng).
* [ ] **Kiểm thử độ động học của hạt (Dynamic Test):** Nhìn kỹ vào màn hình kính ngắm khi đứng im camera. (Yêu cầu: Các hạt nhiễu hạt phải chuyển động nhảy liên tục như nhiễu tivi hoặc phim nhựa cổ điển, không được phép đứng im thành một tấm lưới tĩnh).
* [ ] **Kiểm thử chất lượng ảnh lưu (Output Check):** Bấm nút Shutter chụp ảnh, mở Bộ sưu tập hệ thống ra phóng to ảnh xem các hạt nhiễu có được trộn đều, mịn vào các vùng màu mảng lớn (như vùng tối, mảng tường phẳng) đúng chất liệu Vintage không.