web_haptics 0.0.2 copy "web_haptics: ^0.0.2" to clipboard
web_haptics: ^0.0.2 copied to clipboard

Platformweb

Haptic feedback for Flutter web apps. Uses the iOS Taptic Engine via the checkbox-switch trick and navigator.vibrate() as the Android/fallback path. A Dart port of the web-haptics JavaScript library b [...]

example/lib/main.dart

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:web_haptics/web_haptics.dart';

void main() {
  runApp(const WebHapticsDemo());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'web_haptics',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.grey,
        brightness: Brightness.light,
      ),
      home: const DemoPage(),
    );
  }
}

// ---------------------------------------------------------------------------
// Emoji particle data
// ---------------------------------------------------------------------------

class _Particle {
  _Particle({
    required this.icon,
    required this.startX,
    required this.startY,
    required this.dx,
    required this.dy,
    required this.size,
    required this.rotation,
    required this.controller,
  });

  final IconData icon;
  final double startX;
  final double startY;
  final double dx;
  final double dy;
  final double size;
  final double rotation;
  final AnimationController controller;
}

// ---------------------------------------------------------------------------
// Preset definition
// ---------------------------------------------------------------------------

class _Preset {
  const _Preset(this.name, this.icon, this.particleIcons);

  final String name;
  final IconData icon;
  final List<IconData> particleIcons;
}

// ---------------------------------------------------------------------------
// Demo page
// ---------------------------------------------------------------------------

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

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> with TickerProviderStateMixin {
  late final WebHaptics _haptics;
  String? _activePreset;
  final List<_Particle> _particles = [];
  final _random = Random();

  static const _notificationPresets = [
    _Preset('success', Icons.check_circle_outline,
        [Icons.check, Icons.star_outline, Icons.thumb_up_outlined]),
    _Preset('warning', Icons.warning_amber_rounded,
        [Icons.priority_high, Icons.warning_amber, Icons.report_outlined]),
    _Preset('error', Icons.error_outline,
        [Icons.close, Icons.block, Icons.dangerous_outlined]),
  ];

  static const _impactPresets = [
    _Preset('light', Icons.brightness_low,
        [Icons.light_mode_outlined, Icons.wb_sunny_outlined, Icons.flare]),
    _Preset('medium', Icons.brightness_medium,
        [Icons.circle_outlined, Icons.adjust, Icons.lens_outlined]),
    _Preset('heavy', Icons.brightness_high,
        [Icons.fitness_center, Icons.gavel, Icons.flash_on]),
    _Preset('soft', Icons.cloud_outlined,
        [Icons.cloud_outlined, Icons.spa_outlined, Icons.air]),
    _Preset('rigid', Icons.square_outlined,
        [Icons.square_outlined, Icons.crop_square, Icons.dashboard_outlined]),
  ];

  static const _selectionPresets = [
    _Preset('selection', Icons.touch_app_outlined,
        [Icons.touch_app_outlined, Icons.radio_button_checked, Icons.ads_click]),
  ];

  static const _customPresets = [
    _Preset('nudge', Icons.notifications_active_outlined,
        [Icons.notifications_outlined, Icons.campaign_outlined, Icons.push_pin_outlined]),
    _Preset('buzz', Icons.vibration,
        [Icons.vibration, Icons.waves, Icons.graphic_eq]),
  ];

  @override
  void initState() {
    super.initState();
    _haptics = WebHaptics();
  }

  @override
  void dispose() {
    for (final p in _particles) {
      p.controller.dispose();
    }
    _haptics.destroy();
    super.dispose();
  }

  Future<void> _triggerPreset(String name, Offset globalPosition,
      List<IconData> icons) async {
    setState(() => _activePreset = name);
    _spawnParticles(icons, globalPosition);
    await _haptics.trigger(name);
    await Future.delayed(const Duration(milliseconds: 300));
    if (mounted) setState(() => _activePreset = null);
  }

  void _spawnParticles(List<IconData> icons, Offset position) {
    final count = 5 + _random.nextInt(4);

    for (var i = 0; i < count; i++) {
      final controller = AnimationController(
        duration: Duration(milliseconds: 800 + _random.nextInt(600)),
        vsync: this,
      );

      final particle = _Particle(
        icon: icons[_random.nextInt(icons.length)],
        startX: position.dx - 10 + _random.nextDouble() * 20,
        startY: position.dy - 10,
        dx: (_random.nextDouble() - 0.5) * 120,
        dy: -(80 + _random.nextDouble() * 180),
        size: 16 + _random.nextDouble() * 12,
        rotation: (_random.nextDouble() - 0.5) * 1.2,
        controller: controller,
      );

      controller.addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          if (mounted) {
            setState(() => _particles.remove(particle));
          }
          controller.dispose();
        }
      });

      _particles.add(particle);
      controller.forward();
    }

    setState(() {});
  }

  // -------------------------------------------------------------------------
  // Build
  // -------------------------------------------------------------------------

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Stack(
        clipBehavior: Clip.none,
        children: [
          SafeArea(
            child: SingleChildScrollView(
              padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
              child: Center(
                child: ConstrainedBox(
                  constraints: const BoxConstraints(maxWidth: 500),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Center(child: _buildTitle()),
                      const SizedBox(height: 8),
                      Center(
                        child: Text(
                          'Haptic feedback for Flutter web',
                          style: TextStyle(
                              fontSize: 16, color: Colors.grey[500]),
                        ),
                      ),
                      const SizedBox(height: 16),
                      Center(child: _buildStatusChip()),
                      const SizedBox(height: 36),
                      _buildSection('Notification', _notificationPresets),
                      _buildSection('Impact', _impactPresets),
                      _buildSection('Selection', _selectionPresets),
                      _buildSection('Custom', _customPresets),
                      const SizedBox(height: 24),
                      Center(
                        child: Text(
                          'Dart port of web-haptics by Lochie Axon',
                          style: TextStyle(
                              fontSize: 12, color: Colors.grey[400]),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),

          // Particle overlay
          Positioned.fill(
            child: IgnorePointer(
              child: Stack(
                clipBehavior: Clip.none,
                children: [
                  for (final particle in _particles)
                    AnimatedBuilder(
                      animation: particle.controller,
                      builder: (context, _) {
                        final t = particle.controller.value;
                        final ease = Curves.easeOut.transform(t);
                        return Positioned(
                          left: particle.startX + particle.dx * ease,
                          top: particle.startY + particle.dy * ease,
                          child: Opacity(
                            opacity: (1.0 - t).clamp(0.0, 1.0),
                            child: Transform.rotate(
                              angle: particle.rotation * t,
                              child: Icon(
                                particle.icon,
                                size: particle.size,
                                color: Colors.grey[400],
                              ),
                            ),
                          ),
                        );
                      },
                    ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTitle() {
    return Text(
      'web_haptics',
      style: TextStyle(
        fontSize: 32,
        fontWeight: FontWeight.w700,
        color: Colors.grey[900],
        letterSpacing: -0.5,
      ),
    );
  }

  Widget _buildStatusChip() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: Colors.grey[100],
        borderRadius: BorderRadius.circular(20),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(
            WebHaptics.isSupported ? Icons.vibration : Icons.phone_iphone,
            size: 14,
            color: Colors.grey[600],
          ),
          const SizedBox(width: 6),
          Text(
            WebHaptics.isSupported
                ? 'Vibration API supported'
                : 'Using iOS haptic fallback',
            style: TextStyle(fontSize: 12, color: Colors.grey[600]),
          ),
        ],
      ),
    );
  }

  Widget _buildSection(String title, List<_Preset> presets) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            title,
            style: TextStyle(
              fontSize: 13,
              fontWeight: FontWeight.w600,
              color: Colors.grey[500],
              letterSpacing: 0.5,
            ),
          ),
          const SizedBox(height: 10),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              for (final preset in presets)
                _PresetChip(
                  name: preset.name,
                  icon: preset.icon,
                  isActive: _activePreset == preset.name,
                  onTapUp: (globalPos) =>
                      _triggerPreset(preset.name, globalPos, preset.particleIcons),
                ),
            ],
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Preset chip
// ---------------------------------------------------------------------------

class _PresetChip extends StatelessWidget {
  const _PresetChip({
    required this.name,
    required this.icon,
    required this.isActive,
    required this.onTapUp,
  });

  final String name;
  final IconData icon;
  final bool isActive;
  final void Function(Offset globalPosition) onTapUp;

  @override
  Widget build(BuildContext context) {
    final displayName = name[0].toUpperCase() + name.substring(1);

    return GestureDetector(
      onTapUp: (details) => onTapUp(details.globalPosition),
      child: AnimatedScale(
        scale: isActive ? 0.93 : 1.0,
        duration: const Duration(milliseconds: 100),
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
          decoration: BoxDecoration(
            color: isActive ? Colors.grey[300] : Colors.grey[100],
            borderRadius: BorderRadius.circular(100),
            border: Border.all(
              color: isActive ? Colors.grey[500]! : Colors.grey[300]!,
            ),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Icon(icon, size: 18, color: Colors.grey[700]),
              const SizedBox(width: 8),
              Text(
                displayName,
                style: TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.w500,
                  color: Colors.grey[800],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
3
likes
150
points
137
downloads

Publisher

verified publishersouvikbiswas.com

Weekly Downloads

Haptic feedback for Flutter web apps. Uses the iOS Taptic Engine via the checkbox-switch trick and navigator.vibrate() as the Android/fallback path. A Dart port of the web-haptics JavaScript library by Lochie Axon.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

web

More

Packages that depend on web_haptics