zesage / panorama

Panorama - Flutter Widget
Apache License 2.0
114 stars 71 forks source link

[Future] Add device rotation function #2

Closed lytian closed 4 years ago

lytian commented 4 years ago

As the device rotates, the picture rotates.

omarakhayyat commented 4 years ago

Yessss, this is exactly what I was looking for, I will try to change the code of this package and if it work I will send a pull request.

omarakhayyat commented 4 years ago

hello, I've updated Panorama Package to support gyroscope, but the issue I am facing is how to handle or listen to gyroscope event instead of gesture control? and I need to make a switch/boolean value that will change isGyroscope, like isInteractive property of Panorama. PS. sensors package should be added to pubspec.yaml

library panorama;

import 'dart:async';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_cube/flutter_cube.dart';
import 'package:sensors/sensors.dart';

class Panorama extends StatefulWidget {
  Panorama(
      {Key key,
      this.latitude = 0,
      this.longitude = 0,
      this.zoom = 1.0,
      this.minLatitude = -90.0,
      this.maxLatitude = 90.0,
      this.minLongitude = -180.0,
      this.maxLongitude = 180.0,
      this.minZoom = 1.0,
      this.maxZoom = 5.0,
      this.sensitivity = 1.0,
      this.animSpeed = 1.0,
      this.animReverse = true,
      this.latSegments = 32,
      this.lonSegments = 64,
      this.interactive = true,
      this.gyroscope,
      this.child,
      this.isCompass})
      : super(key: key);

  /// The initial latitude, in degrees, between -90 and 90. default to 0
  final double latitude;

  /// The initial longitude, in degrees, between -180 and 180. default to 0
  final double longitude;

  /// The initial zoom, default to 1.0.
  final double zoom;

  /// The minimal latitude to show. default to -90.0
  final double minLatitude;

  /// The maximal latitude to show. default to 90.0
  final double maxLatitude;

  /// The minimal longitude to show. default to -180.0
  final double minLongitude;

  /// The maximal longitude to show. default to 180.0
  final double maxLongitude;

  /// The minimal zomm. default to 1.0
  final double minZoom;

  /// The maximal zomm. default to 5.0
  final double maxZoom;

  /// The sensitivity of the gesture. default to 1.0
  final double sensitivity;

  /// The Speed of rotation by animation. default to 1.0
  final double animSpeed;

  /// Reverse rotation when the current longitude reaches the minimal or maximum. default to true
  final bool animReverse;

  /// The number of vertical divisions of the sphere.
  final int latSegments;

  /// The number of horizontal divisions of the sphere.
  final int lonSegments;

  /// Interact with the panorama. default to true
  final bool interactive;

  /// Specify an Image(equirectangular image) widget to the panorama.
  final Image child;

  final bool isCompass;
  final Stream gyroscope;

  @override
  _PanoramaState createState() => _PanoramaState();
}

class _PanoramaState extends State<Panorama>
    with SingleTickerProviderStateMixin {
  Scene scene;
  double latitude;
  double longitude;
  double latitudeDelta = 0;
  double longitudeDelta = 0;
  double zoomDelta = 0;
  Offset _lastFocalPoint;
  double _lastZoom;
  double _radius = 500;
  double _dampingFactor = 0.05;
  double _animateDirection = 1.0;
  AnimationController _controller;

  void _handleScaleStart(ScaleStartDetails details) {
    _lastFocalPoint = details.localFocalPoint;
    _lastZoom = null;
  }

  void _handleScaleUpdate(ScaleUpdateDetails details) {
    final offset = details.localFocalPoint - _lastFocalPoint;
    _lastFocalPoint = details.localFocalPoint;
    latitudeDelta += widget.sensitivity *
        0.5 *
        math.pi *
        offset.dy /
        scene.camera.viewportHeight;
    longitudeDelta -= widget.sensitivity *
        _animateDirection *
        0.5 *
        math.pi *
        offset.dx /
        scene.camera.viewportHeight;
    if (_lastZoom == null) {
      _lastZoom = scene.camera.zoom;
    }
    zoomDelta += _lastZoom * details.scale - (scene.camera.zoom + zoomDelta);
    if (!_controller.isAnimating) {
      _controller.reset();
      if (widget.animSpeed != 0) {
        _controller.repeat();
      } else
        _controller.forward();
    }
  }

  void _onSceneCreated(Scene scene) {
    this.scene = scene;
    scene.camera.near = 1.0;
    scene.camera.far = _radius + 1.0;
    scene.camera.fov = 75;
    scene.camera.zoom = widget.zoom;
    scene.camera.position.setFrom(Vector3(0, 0, 0.1));
    setCameraTarget(latitude, longitude);

    if (widget.child != null) {
      loadImageFromProvider(widget.child.image).then((ui.Image image) {
        final Mesh mesh = generateSphereMesh(
            radius: _radius,
            latSegments: widget.latSegments,
            lonSegments: widget.lonSegments,
            texture: image);
        scene.world
            .add(Object(name: 'surface', mesh: mesh, backfaceCulling: false));
        scene.updateTexture();
      });
    }
  }

  void setCameraTarget(double latitude, double longitude) {
    longitude += math.pi;
    scene.camera.target.x = math.cos(longitude) * math.cos(latitude) * _radius;
    scene.camera.target.y = math.sin(latitude) * _radius;
    scene.camera.target.z = math.sin(longitude) * math.cos(latitude) * _radius;
    scene.update();
  }

  @override
  void initState() {
    List<StreamSubscription<dynamic>> _streamSubscriptions =
        <StreamSubscription<dynamic>>[];
    List<double> _gyroscopeValues;

    super.initState();
    latitude = widget.latitude;
    longitude = widget.longitude;

    _controller = AnimationController(
        duration: Duration(milliseconds: 60000), vsync: this)
      ..addListener(() {
        if (scene == null) return;
        longitudeDelta += 0.001 * widget.animSpeed;
        if (latitudeDelta.abs() < 0.001 &&
            longitudeDelta.abs() < 0.001 &&
            zoomDelta.abs() < 0.001) {
          if (widget.animSpeed == 0 && _controller.isAnimating)
            _controller.stop();
          return;
        }

        _streamSubscriptions.add(gyroscopeEvents.listen((GyroscopeEvent event) {
          _gyroscopeValues = <double>[event.x, event.y, event.z];
        }));
        // animate vertical rotating
        latitude +=
            _gyroscopeValues.elementAt(0) * _dampingFactor * widget.sensitivity;
        latitudeDelta *= 1 - _dampingFactor * widget.sensitivity;
        latitude = latitude.clamp(radians(math.max(-89, widget.minLatitude)),
            radians(math.min(89, widget.maxLatitude)));
        // animate horizontal rotating
        longitude += _animateDirection *
            (-_gyroscopeValues.elementAt(1)) *
            _dampingFactor *
            widget.sensitivity;
        longitudeDelta *= 1 - _dampingFactor * widget.sensitivity;
        if (widget.maxLongitude - widget.minLongitude < 360) {
          final double lon = longitude.clamp(
              radians(widget.minLongitude), radians(widget.maxLongitude));
          if (longitude != lon) {
            longitude = lon;
            if (widget.animSpeed != 0) {
              if (widget.animReverse) {
                _animateDirection *= -1.0;
              } else
                _controller.stop();
            }
          }
        }
        // animate zomming
        final double zoom = scene.camera.zoom + zoomDelta * _dampingFactor;
        zoomDelta *= 1 - _dampingFactor;
        scene.camera.zoom = zoom.clamp(widget.minZoom, widget.maxZoom);
        setCameraTarget(latitude, longitude);
      });
    if (widget.animSpeed != 0) _controller.repeat();
  }

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

  @override
  void didUpdateWidget(Panorama oldWidget) {
    super.didUpdateWidget(oldWidget);
    final Object surface = scene.world.find(RegExp('surface'));
    if (surface == null) return;
    if (widget.latSegments != oldWidget.latSegments ||
        widget.lonSegments != oldWidget.lonSegments) {
      surface.mesh = generateSphereMesh(
          radius: _radius,
          latSegments: widget.latSegments,
          lonSegments: widget.lonSegments,
          texture: surface.mesh.texture);
    }
    if (widget.child?.image != oldWidget.child?.image) {
      loadImageFromProvider(widget.child.image).then((ui.Image image) {
        surface.mesh.texture = image;
        surface.mesh.textureRect = Rect.fromLTWH(
            0, 0, image.width.toDouble(), image.height.toDouble());
        scene.updateTexture();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return widget.interactive
        ? GestureDetector(
            onScaleStart: _handleScaleStart,
            onScaleUpdate: _handleScaleUpdate,
            child: Cube(interactive: false, onSceneCreated: _onSceneCreated),
          )
        : Cube(interactive: false, onSceneCreated: _onSceneCreated);
  }
}

Mesh generateSphereMesh(
    {num radius = 1.0,
    int latSegments = 16,
    int lonSegments = 16,
    ui.Image texture}) {
  int count = (latSegments + 1) * (lonSegments + 1);
  List<Vector3> vertices = List<Vector3>(count);
  List<Offset> texcoords = List<Offset>(count);
  List<Polygon> indices = List<Polygon>(latSegments * lonSegments * 2);

  int i = 0;
  for (int y = 0; y <= latSegments; ++y) {
    final double v = y / latSegments;
    final double sv = math.sin(v * math.pi);
    final double cv = math.cos(v * math.pi);
    for (int x = 0; x <= lonSegments; ++x) {
      final double u = x / lonSegments;
      vertices[i] = Vector3(radius * math.cos(u * math.pi * 2.0) * sv,
          radius * cv, radius * math.sin(u * math.pi * 2.0) * sv);
      texcoords[i] = Offset(u, 1.0 - v);
      i++;
    }
  }

  i = 0;
  for (int y = 0; y < latSegments; ++y) {
    final int base1 = (lonSegments + 1) * y;
    final int base2 = (lonSegments + 1) * (y + 1);
    for (int x = 0; x < lonSegments; ++x) {
      indices[i++] = Polygon(base1 + x, base1 + x + 1, base2 + x);
      indices[i++] = Polygon(base1 + x + 1, base2 + x + 1, base2 + x);
    }
  }

  final Mesh mesh = Mesh(
      vertices: vertices,
      texcoords: texcoords,
      indices: indices,
      texture: texture);
  return mesh;
}

/// Get ui.Image from ImageProvider
Future<ui.Image> loadImageFromProvider(ImageProvider provider) async {
  final Completer<ui.Image> completer = Completer<ui.Image>();
  final ImageStream imageStream = provider.resolve(ImageConfiguration());
  ImageStreamListener listener;
  listener = ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
    completer.complete(imageInfo.image);
    imageStream.removeListener(listener);
  });
  imageStream.addListener(listener);
  return completer.future;
}`
lytian commented 4 years ago

Screenshot_20200414-160355 我这里图片大概有16M,然后会出现这样的问题。

lytian commented 4 years ago

有没有考虑过使用Google VR的插件来实现这样的功能呢?

omarakhayyat commented 4 years ago

No I didn't use Google VR because it need a platform code specific work, what I did in the above code is only updating the gestureControl coordinates with gyroscope events, this means when you click on the screen it will update the longitude and latitude from gyroscope sensor and it works.

I’m thinking now how to make this change without the gesture control and to listen only from gyroscope.

If you have any idea please share it.

omarakhayyat commented 4 years ago

hello,

kindly check the updated code for panorama.dart, it is working fine with both gesture and sensors. But now I'm facing a problem with animation, I want it to be animated as of Gesture. like when I'm moving my mobile it is moving with some glitching if we were able to add animation to it; it will be awesome.

library panorama;

import 'dart:async';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_cube/flutter_cube.dart';
import 'package:sensors/sensors.dart';

class Panorama extends StatefulWidget {
  Panorama(
      {Key key,
      this.latitude = 0,
      this.longitude = 0,
      this.zoom = 1.0,
      this.minLatitude = -90.0,
      this.maxLatitude = 90.0,
      this.minLongitude = -180.0,
      this.maxLongitude = 180.0,
      this.minZoom = 1.0,
      this.maxZoom = 5.0,
      this.sensitivity = 1.0,
      this.animSpeed = 1.0,
      this.animReverse = true,
      this.latSegments = 32,
      this.lonSegments = 64,
      this.interactive = true,
      this.gyroscope,
      this.child,
      this.isCompass})
      : super(key: key);

  /// The initial latitude, in degrees, between -90 and 90. default to 0
  final double latitude;

  /// The initial longitude, in degrees, between -180 and 180. default to 0
  final double longitude;

  /// The initial zoom, default to 1.0.
  final double zoom;

  /// The minimal latitude to show. default to -90.0
  final double minLatitude;

  /// The maximal latitude to show. default to 90.0
  final double maxLatitude;

  /// The minimal longitude to show. default to -180.0
  final double minLongitude;

  /// The maximal longitude to show. default to 180.0
  final double maxLongitude;

  /// The minimal zomm. default to 1.0
  final double minZoom;

  /// The maximal zomm. default to 5.0
  final double maxZoom;

  /// The sensitivity of the gesture. default to 1.0
  final double sensitivity;

  /// The Speed of rotation by animation. default to 1.0
  final double animSpeed;

  /// Reverse rotation when the current longitude reaches the minimal or maximum. default to true
  final bool animReverse;

  /// The number of vertical divisions of the sphere.
  final int latSegments;

  /// The number of horizontal divisions of the sphere.
  final int lonSegments;

  /// Interact with the panorama. default to true
  final bool interactive;

  /// Specify an Image(equirectangular image) widget to the panorama.
  final Image child;

  final bool isCompass;
  final Stream gyroscope;

  @override
  _PanoramaState createState() => _PanoramaState();
}

class _PanoramaState extends State<Panorama>
    with SingleTickerProviderStateMixin {
  Scene scene;
  double latitude;
  double longitude;
  double latitudeDelta = 0;
  double longitudeDelta = 0;
  double zoomDelta = 0;
  Offset _lastFocalPoint;
  double _lastZoom;
  double _radius = 500;
  double _dampingFactor = 0.05;
  double _animateDirection = 1.0;
  AnimationController _controller;
  List<StreamSubscription<dynamic>> _streamSubscriptions =
      <StreamSubscription<dynamic>>[];
  List<double> _gyroscopeValues;

  void _handleScaleStart(ScaleStartDetails details) {
    _lastFocalPoint = details.localFocalPoint;
    _lastZoom = null;
  }

  void _handleScaleUpdate(ScaleUpdateDetails details) {
    final offset = details.localFocalPoint - _lastFocalPoint;
    _lastFocalPoint = details.localFocalPoint;
    latitudeDelta += widget.sensitivity *
        0.5 *
        math.pi *
        offset.dy /
        scene.camera.viewportHeight;
    longitudeDelta -= widget.sensitivity *
        _animateDirection *
        0.5 *
        math.pi *
        offset.dx /
        scene.camera.viewportHeight;
    if (_lastZoom == null) {
      _lastZoom = scene.camera.zoom;
    }
    zoomDelta += _lastZoom * details.scale - (scene.camera.zoom + zoomDelta);
    if (!_controller.isAnimating) {
      _controller.reset();
      if (widget.animSpeed != 0) {
        _controller.repeat();
      } else
        _controller.forward();
    }
  }

  void _onSceneCreated(Scene scene) {
    this.scene = scene;
    scene.camera.near = 1.0;
    scene.camera.far = _radius + 1.0;
    scene.camera.fov = 75;
    scene.camera.zoom = widget.zoom;
    scene.camera.position.setFrom(Vector3(0, 0, 0.1));
    setCameraTarget(latitude, longitude);

    if (widget.child != null) {
      loadImageFromProvider(widget.child.image).then((ui.Image image) {
        final Mesh mesh = generateSphereMesh(
            radius: _radius,
            latSegments: widget.latSegments,
            lonSegments: widget.lonSegments,
            texture: image);
        scene.world
            .add(Object(name: 'surface', mesh: mesh, backfaceCulling: false));
        scene.updateTexture();
      });
    }
  }

  void setCameraTarget(double latitude, double longitude) {
    longitude += math.pi;
    scene.camera.target.x = math.cos(longitude) * math.cos(latitude) * _radius;
    scene.camera.target.y = math.sin(latitude) * _radius;
    scene.camera.target.z = math.sin(longitude) * math.cos(latitude) * _radius;
    scene.update();
  }

  @override
  void initState() {
    super.initState();

    latitude = widget.latitude;
    longitude = widget.longitude;

    _controller = AnimationController(
        duration: Duration(milliseconds: 60000), vsync: this)
      ..addListener(() {
        if (scene == null) return;
        longitudeDelta += 0.001 * widget.animSpeed;
        if (latitudeDelta.abs() < 0.001 &&
            longitudeDelta.abs() < 0.001 &&
            zoomDelta.abs() < 0.001) {
          if (widget.animSpeed == 0 && _controller.isAnimating)
            _controller.stop();
          return;
        }

        // animate vertical rotating
        latitude += latitudeDelta * _dampingFactor * widget.sensitivity;
        latitudeDelta *= 1 - _dampingFactor * widget.sensitivity;
        latitude = latitude.clamp(radians(math.max(-89, widget.minLatitude)),
            radians(math.min(89, widget.maxLatitude)));
        // animate horizontal rotating
        longitude += _animateDirection *
            longitudeDelta *
            _dampingFactor *
            widget.sensitivity;
        longitudeDelta *= 1 - _dampingFactor * widget.sensitivity;
        if (widget.maxLongitude - widget.minLongitude < 360) {
          final double lon = longitude.clamp(
              radians(widget.minLongitude), radians(widget.maxLongitude));
          if (longitude != lon) {
            longitude = lon;
            if (widget.animSpeed != 0) {
              if (widget.animReverse) {
                _animateDirection *= -1.0;
              } else
                _controller.stop();
            }
          }
        }
        // animate zomming
        final double zoom = scene.camera.zoom + zoomDelta * _dampingFactor;
        zoomDelta *= 1 - _dampingFactor;
        scene.camera.zoom = zoom.clamp(widget.minZoom, widget.maxZoom);
        setCameraTarget(latitude, longitude);
      });
    if (widget.animSpeed != 0) _controller.repeat();

    _streamSubscriptions.add(
      gyroscopeEvents.listen(
        (GyroscopeEvent event) {
          _gyroscopeValues = <double>[event.x, event.y, event.z];
          print(_gyroscopeValues.elementAt(0));
          if (widget.isCompass) {
            latitude += _gyroscopeValues.elementAt(0) *
                5 *
                _dampingFactor *
                widget.sensitivity;
            latitudeDelta *= 1 - _dampingFactor * widget.sensitivity;
            latitude = latitude.clamp(
                radians(math.max(-89, widget.minLatitude)),
                radians(math.min(89, widget.maxLatitude)));

            longitude += _animateDirection *
                (-_gyroscopeValues.elementAt(1)) *
                5 *
                _dampingFactor *
                widget.sensitivity;
            longitudeDelta *= 1 - _dampingFactor * widget.sensitivity;
            if (widget.maxLongitude - widget.minLongitude < 360) {
              final double lon = longitude.clamp(
                  radians(widget.minLongitude), radians(widget.maxLongitude));
              if (longitude != lon) {
                longitude = lon;
                if (widget.animSpeed != 0) {
                  if (widget.animReverse) {
                    _animateDirection *= -1.0;
                  } else
                    _controller.stop();
                }
              }
            }
            final double zoom = scene.camera.zoom + zoomDelta * _dampingFactor;
            zoomDelta *= 1 - _dampingFactor;
            scene.camera.zoom = zoom.clamp(widget.minZoom, widget.maxZoom);
            if (!_controller.isAnimating) {
              _controller.reset();
              if (widget.animSpeed != 0) {
                _controller.repeat();
              } else
                _controller.forward();
            }
            setCameraTarget(latitude, longitude);
          }
        },
      ),
    );
  }

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

  @override
  void didUpdateWidget(Panorama oldWidget) {
    super.didUpdateWidget(oldWidget);
    final Object surface = scene.world.find(RegExp('surface'));
    if (surface == null) return;
    if (widget.latSegments != oldWidget.latSegments ||
        widget.lonSegments != oldWidget.lonSegments) {
      surface.mesh = generateSphereMesh(
          radius: _radius,
          latSegments: widget.latSegments,
          lonSegments: widget.lonSegments,
          texture: surface.mesh.texture);
    }
    if (widget.child?.image != oldWidget.child?.image) {
      loadImageFromProvider(widget.child.image).then((ui.Image image) {
        surface.mesh.texture = image;
        surface.mesh.textureRect = Rect.fromLTWH(
            0, 0, image.width.toDouble(), image.height.toDouble());
        scene.updateTexture();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return widget.interactive
        ? GestureDetector(
            onScaleStart: _handleScaleStart,
            onScaleUpdate: _handleScaleUpdate,
            child: Cube(interactive: false, onSceneCreated: _onSceneCreated),
          )
        : Cube(interactive: false, onSceneCreated: _onSceneCreated);
  }
}

Mesh generateSphereMesh(
    {num radius = 1.0,
    int latSegments = 16,
    int lonSegments = 16,
    ui.Image texture}) {
  int count = (latSegments + 1) * (lonSegments + 1);
  List<Vector3> vertices = List<Vector3>(count);
  List<Offset> texcoords = List<Offset>(count);
  List<Polygon> indices = List<Polygon>(latSegments * lonSegments * 2);

  int i = 0;
  for (int y = 0; y <= latSegments; ++y) {
    final double v = y / latSegments;
    final double sv = math.sin(v * math.pi);
    final double cv = math.cos(v * math.pi);
    for (int x = 0; x <= lonSegments; ++x) {
      final double u = x / lonSegments;
      vertices[i] = Vector3(radius * math.cos(u * math.pi * 2.0) * sv,
          radius * cv, radius * math.sin(u * math.pi * 2.0) * sv);
      texcoords[i] = Offset(u, 1.0 - v);
      i++;
    }
  }

  i = 0;
  for (int y = 0; y < latSegments; ++y) {
    final int base1 = (lonSegments + 1) * y;
    final int base2 = (lonSegments + 1) * (y + 1);
    for (int x = 0; x < lonSegments; ++x) {
      indices[i++] = Polygon(base1 + x, base1 + x + 1, base2 + x);
      indices[i++] = Polygon(base1 + x + 1, base2 + x + 1, base2 + x);
    }
  }

  final Mesh mesh = Mesh(
      vertices: vertices,
      texcoords: texcoords,
      indices: indices,
      texture: texture);
  return mesh;
}

/// Get ui.Image from ImageProvider
Future<ui.Image> loadImageFromProvider(ImageProvider provider) async {
  final Completer<ui.Image> completer = Completer<ui.Image>();
  final ImageStream imageStream = provider.resolve(ImageConfiguration());
  ImageStreamListener listener;
  listener = ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
    completer.complete(imageInfo.image);
    imageStream.removeListener(listener);
  });
  imageStream.addListener(listener);
  return completer.future;
}
lytian commented 4 years ago

@omarakhayyat Think you. It works, but it not smooth.

omarakhayyat commented 4 years ago

@lytian yess exactly, because we should add animation controller to it, can you please assist me with this? how to add and enable animation controller to this!!

lytian commented 4 years ago

I did this using the Google VR plugin. It's just a simple demo, and it's being perfected

flutter_panorama

use it

MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
//          child: FlutterPanorama.assets("images/xishui_pano.jpg"),
          child: FlutterPanorama.network('https://storage.googleapis.com/vrview/examples/coral.jpg',
            imageType: ImageType.MEDIA_STEREO_TOP_BOTTOM,
            onImageLoaded: (state) {
              print("------------------------------- ${state == 1 ? '图片加载完成' : '图片加载失败'}");
            },
          ),
        )
      ),