# Tratext Showcase Video — Flutter Implementation Guide

A practical, copy-pasteable guide for porting the 30-second vertical product
video into your Flutter app (or generating it as an MP4 from Flutter).

---

## 1. What you're porting

A **30-second 9:16 vertical video** (1080×1920) made of 6 timed scenes that
showcase the B2C order flow. Everything is built with vector primitives and
the real Tratext logo asset — no third-party motion libraries required.

| # | Range  | Scene             | Key motion                                         |
|---|--------|-------------------|----------------------------------------------------|
| 1 | 0–4s   | Logo intro        | Logo scales in → wordmark → tagline                |
| 2 | 4–9s   | Home + tap +      | Phone slides up, cursor flies to FAB and taps      |
| 3 | 9–15s  | Upload + langs    | Upload progress 0→100%, EN↔DE language pair flips  |
| 4 | 15–21s | Order options     | Cursor taps Certified, then Express                |
| 5 | 21–26s | Status tracker    | Submitted → Review → Translating → Delivered + 🎉  |
| 6 | 26–30s | End card          | Green wipe, "Translation, delivered." + CTA       |

---

## 2. Two ways to use this in Flutter

### Option A — Render once, ship the MP4
Easiest path: record the HTML version once with a screen recorder (or
`puppeteer` + `ffmpeg` server-side), then play it in-app with
[`video_player`](https://pub.dev/packages/video_player) or
[`chewie`](https://pub.dev/packages/chewie). Use this for marketing /
splash / onboarding screens where you don't need interaction.

### Option B — Native Flutter animation (recommended for in-app onboarding)
Rebuild the scenes with Flutter widgets and `AnimationController`. You get
crisp vectors, perfect Poppins rendering, and the user can skip / replay.
The rest of this guide covers Option B.

---

## 3. Project setup

### `pubspec.yaml`

```yaml
name: tratext_video
description: Tratext showcase video screen.

environment:
  sdk: ">=3.0.0 <4.0.0"
  flutter: ">=3.13.0"

dependencies:
  flutter:
    sdk: flutter
  google_fonts: ^6.2.1   # for Poppins
  # Or bundle Poppins as a local font (preferred for offline):
  # fonts: ./assets/fonts/Poppins-*.ttf

flutter:
  uses-material-design: true
  assets:
    - assets/tratext_logo.png
  fonts:
    - family: Poppins
      fonts:
        - asset: assets/fonts/Poppins-Regular.ttf
        - asset: assets/fonts/Poppins-Medium.ttf
          weight: 500
        - asset: assets/fonts/Poppins-SemiBold.ttf
          weight: 600
        - asset: assets/fonts/Poppins-Bold.ttf
          weight: 700
```

Drop `assets/tratext_logo.png` (already exported in this project at
`flutter/assets/tratext_logo.png`) into your Flutter project.

---

## 4. Design tokens

Mirrors the Tratext design system colors used in the video:

```dart
// lib/tratext/tokens.dart
import 'package:flutter/material.dart';

class TxColor {
  static const green     = Color(0xFF2E7D32); // primary
  static const greenDeep = Color(0xFF1B5E20);
  static const greenSoft = Color(0xFFCCE8D9);
  static const greenTint = Color(0xFFE8F4ED);
  static const text      = Color(0xFF404040);
  static const mid       = Color(0xFF616161);
  static const light     = Color(0xFF9E9E9E);
  static const border    = Color(0xFFE0E0E0);
  static const bg        = Color(0xFFF5F5F5);
  static const info      = Color(0xFF0D7BE5);
}
```

---

## 5. Architecture

```
lib/
├── main.dart
├── tratext/
│   ├── tokens.dart
│   ├── video_screen.dart           // top-level controller + scene switcher
│   ├── widgets/
│   │   ├── tratext_mark.dart       // logo widget (color + white silhouette)
│   │   ├── phone_frame.dart        // device bezel + status bar
│   │   ├── progress_dots.dart      // top-of-screen scene indicator
│   │   └── confetti.dart
│   ├── screens/                    // mock app screens shown inside the phone
│   │   ├── home_screen.dart
│   │   ├── upload_screen.dart
│   │   ├── summary_screen.dart
│   │   └── status_screen.dart
│   └── scenes/                     // one widget per timed scene
│       ├── scene_intro.dart
│       ├── scene_tap_plus.dart
│       ├── scene_upload.dart
│       ├── scene_options.dart
│       ├── scene_status.dart
│       └── scene_end.dart
```

---

## 6. The timing controller

A single `AnimationController` drives all 30 seconds. Each scene reads the
master `time` (in seconds) and computes its own state. This mirrors how the
HTML version uses `<Stage>` + `<Sprite>`.

```dart
// lib/tratext/video_screen.dart
import 'package:flutter/material.dart';

class TratextVideoScreen extends StatefulWidget {
  const TratextVideoScreen({super.key});
  @override
  State<TratextVideoScreen> createState() => _TratextVideoScreenState();
}

class _TratextVideoScreenState extends State<TratextVideoScreen>
    with SingleTickerProviderStateMixin {
  static const totalSeconds = 30;
  late final AnimationController _ctrl;

  // Scene cuts in seconds.
  static const cuts = [0, 4, 9, 15, 21, 26, 30];

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: const Duration(seconds: totalSeconds),
    )..repeat(); // or ..forward() for one-shot
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: AnimatedBuilder(
          animation: _ctrl,
          builder: (context, _) {
            final t = _ctrl.value * totalSeconds; // 0..30 seconds
            return _StageScaler(
              designWidth: 1080,
              designHeight: 1920,
              child: _buildSceneAt(t),
            );
          },
        ),
      ),
    );
  }

  Widget _buildSceneAt(double t) {
    // Pick the active scene; pass scene-local time so each scene can compute
    // its own animation starting from 0.
    if (t < 4)  return SceneIntro(localTime: t);
    if (t < 9)  return SceneTapPlus(localTime: t - 4);
    if (t < 15) return SceneUpload(localTime: t - 9);
    if (t < 21) return SceneOptions(localTime: t - 15);
    if (t < 26) return SceneStatus(localTime: t - 21);
    return SceneEnd(localTime: t - 26);
  }
}

/// Letterboxes a fixed 1080×1920 design into any viewport via Transform.scale.
class _StageScaler extends StatelessWidget {
  const _StageScaler({
    required this.designWidth,
    required this.designHeight,
    required this.child,
  });
  final double designWidth;
  final double designHeight;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (ctx, c) {
        final scale = (c.maxWidth / designWidth)
            .clamp(0.0, c.maxHeight / designHeight);
        return Center(
          child: SizedBox(
            width: designWidth * scale,
            height: designHeight * scale,
            child: FittedBox(
              fit: BoxFit.contain,
              child: SizedBox(
                width: designWidth,
                height: designHeight,
                child: child,
              ),
            ),
          ),
        );
      },
    );
  }
}
```

---

## 7. Easing & interpolation helpers

The HTML version uses `interpolate([keyTimes], [keyValues])`. Flutter's
`Curves` plus a tiny helper give you the same thing:

```dart
// lib/tratext/tween_helpers.dart
import 'dart:math' as math;
import 'package:flutter/animation.dart';

double clamp01(double x) => x.clamp(0.0, 1.0);

/// Multi-keyframe interpolation — t goes through [inputs], returns the
/// matching point along [outputs] with optional per-segment easing.
double interpolate(
  double t,
  List<double> inputs,
  List<double> outputs, {
  Curve curve = Curves.linear,
}) {
  assert(inputs.length == outputs.length && inputs.length >= 2);
  if (t <= inputs.first) return outputs.first;
  if (t >= inputs.last)  return outputs.last;
  for (var i = 0; i < inputs.length - 1; i++) {
    if (t >= inputs[i] && t <= inputs[i + 1]) {
      final span = inputs[i + 1] - inputs[i];
      final local = span == 0 ? 0.0 : (t - inputs[i]) / span;
      final eased = curve.transform(local);
      return outputs[i] + (outputs[i + 1] - outputs[i]) * eased;
    }
  }
  return outputs.last;
}

/// Single-segment tween: returns `from` before [start], `to` after [end].
double animate({
  required double t,
  double from = 0,
  double to = 1,
  required double start,
  required double end,
  Curve curve = Curves.easeInOutCubic,
}) {
  if (t <= start) return from;
  if (t >= end) return to;
  return from + (to - from) * curve.transform((t - start) / (end - start));
}
```

---

## 8. The logo widget

```dart
// lib/tratext/widgets/tratext_mark.dart
import 'package:flutter/material.dart';

class TratextMark extends StatelessWidget {
  const TratextMark({
    super.key,
    this.size = 36,
    this.variant = TratextMarkVariant.color,
  });
  final double size;
  final TratextMarkVariant variant;

  @override
  Widget build(BuildContext context) {
    if (variant == TratextMarkVariant.white) {
      // White silhouette via a ColorFiltered mask so the same PNG works
      // on dark/green backgrounds without losing the speech-bubble shape.
      return ShaderMask(
        blendMode: BlendMode.srcIn,
        shaderCallback: (rect) =>
            const LinearGradient(colors: [Colors.white, Colors.white])
                .createShader(rect),
        child: Image.asset(
          'assets/tratext_logo.png',
          width: size,
          height: size,
          fit: BoxFit.contain,
        ),
      );
    }
    return Image.asset(
      'assets/tratext_logo.png',
      width: size,
      height: size,
      fit: BoxFit.contain,
    );
  }
}

enum TratextMarkVariant { color, white }
```

---

## 9. A scene example — the intro (0–4s)

This is a direct port of the HTML `SceneIntro`. Other scenes follow the
same pattern: take `localTime` as input, compute opacity / transform with
`interpolate()`, render.

```dart
// lib/tratext/scenes/scene_intro.dart
import 'package:flutter/material.dart';
import '../tokens.dart';
import '../tween_helpers.dart';
import '../widgets/tratext_mark.dart';

class SceneIntro extends StatelessWidget {
  const SceneIntro({super.key, required this.localTime});
  final double localTime;

  @override
  Widget build(BuildContext context) {
    final logoScale = interpolate(
      localTime, [0, 0.4, 1.0, 3.4, 4.0], [0.4, 1.05, 1.0, 1.0, 1.06],
      curve: Curves.easeOutCubic,
    );
    final logoOpacity = interpolate(
      localTime, [0, 0.4, 3.4, 4.0], [0, 1, 1, 0],
      curve: Curves.easeInOutCubic,
    );
    final wordmarkOpacity = interpolate(
      localTime, [0.5, 1.0, 3.4, 4.0], [0, 1, 1, 0],
      curve: Curves.easeInOutCubic,
    );
    final taglineOpacity = interpolate(
      localTime, [1.2, 1.7, 3.4, 4.0], [0, 1, 1, 0],
      curve: Curves.easeInOutCubic,
    );
    final taglineY = interpolate(
      localTime, [1.2, 1.7], [12, 0],
      curve: Curves.easeOutBack,
    );

    return Stack(children: [
      // Full-bleed white background
      Container(color: Colors.white),
      // Soft green halo
      Center(
        child: Container(
          width: 900, height: 900,
          decoration: const BoxDecoration(
            shape: BoxShape.circle,
            gradient: RadialGradient(colors: [
              Color(0x99CCE8D9), // 60% greenSoft
              Color(0x00FFFFFF),
            ], stops: [0.0, 0.7]),
          ),
        ),
      ),
      // Mark
      Positioned.fill(
        child: Align(
          alignment: const Alignment(0, -0.16), // ~42% from top
          child: Opacity(
            opacity: logoOpacity,
            child: Transform.scale(
              scale: logoScale,
              child: const TratextMark(size: 240),
            ),
          ),
        ),
      ),
      // Wordmark
      Positioned.fill(
        child: Align(
          alignment: const Alignment(0, 0.20),
          child: Opacity(
            opacity: wordmarkOpacity,
            child: Text(
              'tratext',
              style: TextStyle(
                fontFamily: 'Poppins',
                fontSize: 96,
                fontWeight: FontWeight.w700,
                color: TxColor.text,
                letterSpacing: -1.92, // -0.02em at 96px
              ),
            ),
          ),
        ),
      ),
      // Tagline
      Positioned.fill(
        child: Align(
          alignment: const Alignment(0, 0.40),
          child: Opacity(
            opacity: taglineOpacity,
            child: Transform.translate(
              offset: Offset(0, taglineY),
              child: Text(
                'Translation, on demand.',
                style: TextStyle(
                  fontFamily: 'Poppins',
                  fontSize: 36,
                  fontWeight: FontWeight.w400,
                  color: TxColor.mid,
                  letterSpacing: 0.72,
                ),
              ),
            ),
          ),
        ),
      ),
    ]);
  }
}
```

The other scenes (`SceneTapPlus`, `SceneUpload`, `SceneOptions`,
`SceneStatus`, `SceneEnd`) follow the same recipe — see the corresponding
sections in `tratext-scenes.jsx` and translate keyframes 1:1.

---

## 10. Phone frame & in-screen mockups

The `PhoneFrame` is just a rounded black rectangle with a white inner area
and an iOS-style status bar / dynamic island on top. Each mock screen
(home, upload, summary, status) is a normal Flutter widget composed of
`Container`, `Row`, `Column`, `BoxDecoration` — there is nothing exotic.

Tip: build the phone screens as a separate library you can reuse in real
onboarding screens later, not just the video.

---

## 11. Confetti

Use [`confetti`](https://pub.dev/packages/confetti) for a one-line answer:

```dart
final _confetti = ConfettiController(duration: const Duration(seconds: 2));
// in SceneStatus, when localTime crosses 3.0s:
if (localTime > 3.0 && !_confetti.state.isPlaying) _confetti.play();

ConfettiWidget(
  confettiController: _confetti,
  blastDirectionality: BlastDirectionality.explosive,
  colors: const [TxColor.green, Color(0xFF66BB6A), Color(0xFFFFBB33),
                 TxColor.info, Color(0xFFEA5052)],
  numberOfParticles: 40,
)
```

---

## 12. Exporting an MP4 from Flutter (optional)

If marketing wants the video as a downloadable file:

1. Run the screen on a device or emulator at 1080×1920.
2. Use [`screen_recorder`](https://pub.dev/packages/screen_recorder) **or**
   record the platform screen with `adb screenrecord` / iOS QuickTime
   while playing the animation full-screen.
3. Trim and convert with `ffmpeg`:
   ```bash
   ffmpeg -i raw.mov -vf "scale=1080:1920" -c:v libx264 -crf 18 -preset slow tratext_30s.mp4
   ```

For a CI-friendly headless render, the easiest route is actually to keep
the HTML version and run it in `puppeteer` + `ffmpeg` — fewer Flutter
moving parts, identical result.

---

## 13. Suggested integration points in your app

- **First-run onboarding** — show the 30s video once with a Skip button.
- **Empty states** — embed individual scenes (e.g. `SceneStatus`) in the
  Activity tab when a user has no orders yet.
- **Marketing links** — link to a hosted version of the HTML for sharing
  to social.

---

## 14. What's NOT in this guide (yet) — and what to ask me for next

- Full Dart ports of scenes 2–6 (I've ported the structure and given you
  Scene 1 in full; the rest are mechanical translations of the JS
  keyframes — happy to write them out on request).
- Voice-over track + subtitles.
- Localized variants (DE / AR / FR copy decks).
- 16:9 and 1:1 cuts of the same video.

Just say which you want next.
