modified damping strategy

This commit is contained in:
Thibault Deckers 2023-01-14 16:23:00 +01:00
parent 47442e5102
commit 89fbc3f1ec
2 changed files with 96 additions and 75 deletions

View file

@ -1,7 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:panorama/panorama.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:panorama/panorama.dart';
void main() => runApp(MyApp()); void main() => runApp(MyApp());
@ -75,7 +76,6 @@ class _MyHomePageState extends State<MyHomePage> {
switch (_panoId % panoImages.length) { switch (_panoId % panoImages.length) {
case 0: case 0:
panorama = Panorama( panorama = Panorama(
animSpeed: 1.0,
sensorControl: SensorControl.Orientation, sensorControl: SensorControl.Orientation,
onViewChanged: onViewChanged, onViewChanged: onViewChanged,
onTap: (longitude, latitude, tilt) => print('onTap: $longitude, $latitude, $tilt'), onTap: (longitude, latitude, tilt) => print('onTap: $longitude, $latitude, $tilt'),
@ -110,7 +110,6 @@ class _MyHomePageState extends State<MyHomePage> {
break; break;
case 2: case 2:
panorama = Panorama( panorama = Panorama(
animSpeed: 1.0,
sensorControl: SensorControl.Orientation, sensorControl: SensorControl.Orientation,
onViewChanged: onViewChanged, onViewChanged: onViewChanged,
croppedArea: Rect.fromLTWH(2533.0, 1265.0, 5065.0, 2533.0), croppedArea: Rect.fromLTWH(2533.0, 1265.0, 5065.0, 2533.0),
@ -130,7 +129,6 @@ class _MyHomePageState extends State<MyHomePage> {
break; break;
default: default:
panorama = Panorama( panorama = Panorama(
animSpeed: 1.0,
sensorControl: SensorControl.Orientation, sensorControl: SensorControl.Orientation,
onViewChanged: onViewChanged, onViewChanged: onViewChanged,
child: panoImages[_panoId % panoImages.length], child: panoImages[_panoId % panoImages.length],

View file

@ -1,8 +1,9 @@
library panorama; library panorama;
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui;
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_cube/flutter_cube.dart'; import 'package:flutter_cube/flutter_cube.dart';
import 'package:motion_sensors/motion_sensors.dart'; import 'package:motion_sensors/motion_sensors.dart';
@ -30,8 +31,7 @@ class Panorama extends StatefulWidget {
this.maxLongitude = 180.0, this.maxLongitude = 180.0,
this.minZoom = 1.0, this.minZoom = 1.0,
this.maxZoom = 5.0, this.maxZoom = 5.0,
this.sensitivity = 1.0, this.panInertia = 0.05,
this.animSpeed = 0.0,
this.animReverse = true, this.animReverse = true,
this.latSegments = 32, this.latSegments = 32,
this.lonSegments = 64, this.lonSegments = 64,
@ -77,11 +77,8 @@ class Panorama extends StatefulWidget {
/// The maximal zomm. default to 5.0 /// The maximal zomm. default to 5.0
final double maxZoom; final double maxZoom;
/// The sensitivity of the gesture. default to 1.0 /// default to 0.05
final double sensitivity; final double panInertia;
/// The Speed of rotation by animation. default to 0.0
final double animSpeed;
/// Reverse rotation when the current longitude reaches the minimal or maximum. default to true /// Reverse rotation when the current longitude reaches the minimal or maximum. default to true
final bool animReverse; final bool animReverse;
@ -121,7 +118,7 @@ class Panorama extends StatefulWidget {
/// This event will be called when the user has stopped a long presses, it contains latitude and longitude about where the user pressed. /// This event will be called when the user has stopped a long presses, it contains latitude and longitude about where the user pressed.
final Function(double longitude, double latitude, double tilt)? onLongPressEnd; final Function(double longitude, double latitude, double tilt)? onLongPressEnd;
/// This event will be called when provided image is loaded on texture. /// This event will be called when provided image is loaded on texture.
final Function()? onImageLoad; final Function()? onImageLoad;
@ -145,8 +142,6 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
double zoomDelta = 0; double zoomDelta = 0;
late Offset _lastFocalPoint; late Offset _lastFocalPoint;
double? _lastZoom; double? _lastZoom;
double _radius = 500;
double _dampingFactor = 0.05;
double _animateDirection = 1.0; double _animateDirection = 1.0;
late AnimationController _controller; late AnimationController _controller;
double screenOrientationRad = 0.0; double screenOrientationRad = 0.0;
@ -156,6 +151,12 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
late StreamController<Null> _streamController; late StreamController<Null> _streamController;
Stream<Null>? _stream; Stream<Null>? _stream;
ImageStream? _imageStream; ImageStream? _imageStream;
bool _scaling = false;
static const double _halfPi = math.pi * .5;
static const double _epsilon = .001;
static const double _radius = 500;
static const double _panReactivity = .8;
void _handleTapUp(TapUpDetails details) { void _handleTapUp(TapUpDetails details) {
final Vector3 o = positionToLatLon(details.localPosition.dx, details.localPosition.dy); final Vector3 o = positionToLatLon(details.localPosition.dx, details.localPosition.dy);
@ -180,47 +181,63 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
void _handleScaleStart(ScaleStartDetails details) { void _handleScaleStart(ScaleStartDetails details) {
_lastFocalPoint = details.localFocalPoint; _lastFocalPoint = details.localFocalPoint;
_lastZoom = null; _lastZoom = null;
_scaling = true;
} }
void _handleScaleUpdate(ScaleUpdateDetails details) { void _handleScaleUpdate(ScaleUpdateDetails details) {
final offset = details.localFocalPoint - _lastFocalPoint; final offset = details.localFocalPoint - _lastFocalPoint;
_lastFocalPoint = details.localFocalPoint; _lastFocalPoint = details.localFocalPoint;
latitudeDelta += widget.sensitivity * 0.5 * math.pi * offset.dy / scene!.camera.viewportHeight; _updatePositionDeltaForOffset(offset);
longitudeDelta -= widget.sensitivity * _animateDirection * 0.5 * math.pi * offset.dx / scene!.camera.viewportHeight;
if (_lastZoom == null) { final zoom = scene!.camera.zoom;
_lastZoom = scene!.camera.zoom; _lastZoom ??= zoom;
} zoomDelta += _lastZoom! * details.scale - (zoom + zoomDelta);
zoomDelta += _lastZoom! * details.scale - (scene!.camera.zoom + zoomDelta);
if (widget.sensorControl == SensorControl.None && !_controller.isAnimating) { if (widget.sensorControl == SensorControl.None && !_controller.isAnimating) {
_controller.reset(); _controller.reset();
if (widget.animSpeed != 0) { _controller.forward();
_controller.repeat();
} else
_controller.forward();
} }
} }
void _handleScaleEnd(ScaleEndDetails details) {
final offset = details.velocity.pixelsPerSecond / 10;
_updatePositionDeltaForOffset(offset);
_scaling = false;
}
void _updatePositionDeltaForOffset(ui.Offset offset) {
final camera = scene!.camera;
final sensitivity = 1 / camera.zoom;
final viewportHeight = camera.viewportHeight;
latitudeDelta += sensitivity * _halfPi * offset.dy / viewportHeight;
longitudeDelta -= sensitivity * _animateDirection * _halfPi * offset.dx / viewportHeight;
}
void _updateView() { void _updateView() {
if (scene == null) return; if (scene == null) return;
// auto rotate
longitudeDelta += 0.001 * widget.animSpeed; final camera = scene!.camera;
final damping = _scaling ? _panReactivity : widget.panInertia;
// animate vertical rotating // animate vertical rotating
latitudeRad += latitudeDelta * _dampingFactor * widget.sensitivity; latitudeRad += latitudeDelta * damping;
latitudeDelta *= 1 - _dampingFactor * widget.sensitivity; latitudeDelta *= 1 - damping;
// animate horizontal rotating // animate horizontal rotating
longitudeRad += _animateDirection * longitudeDelta * _dampingFactor * widget.sensitivity; longitudeRad += _animateDirection * longitudeDelta * damping;
longitudeDelta *= 1 - _dampingFactor * widget.sensitivity; longitudeDelta *= 1 - damping;
// animate zomming
final double zoom = scene!.camera.zoom + zoomDelta * _dampingFactor; // animate zooming
zoomDelta *= 1 - _dampingFactor; final double zoom = camera.zoom + zoomDelta * damping;
scene!.camera.zoom = zoom.clamp(widget.minZoom, widget.maxZoom); zoomDelta *= 1 - damping;
camera.zoom = zoom.clamp(widget.minZoom, widget.maxZoom);
// stop animation if not needed // stop animation if not needed
if (latitudeDelta.abs() < 0.001 && if (latitudeDelta.abs() < _epsilon && longitudeDelta.abs() < _epsilon && zoomDelta.abs() < _epsilon) {
longitudeDelta.abs() < 0.001 && if (widget.sensorControl == SensorControl.None && _controller.isAnimating) {
zoomDelta.abs() < 0.001) { _controller.stop();
if (widget.sensorControl == SensorControl.None && }
widget.animSpeed == 0 &&
_controller.isAnimating) _controller.stop();
} }
// rotate for screen orientation // rotate for screen orientation
@ -228,7 +245,7 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
// rotate for device orientation // rotate for device orientation
q *= Quaternion.euler(-orientation.z, -orientation.y, -orientation.x); q *= Quaternion.euler(-orientation.z, -orientation.y, -orientation.x);
// rotate to latitude zero // rotate to latitude zero
q *= Quaternion.axisAngle(Vector3(1, 0, 0), math.pi * 0.5); q *= Quaternion.axisAngle(Vector3(1, 0, 0), _halfPi);
// check and limit the rotation range // check and limit the rotation range
Vector3 o = quaternionToOrientation(q); Vector3 o = quaternionToOrientation(q);
@ -243,13 +260,6 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
if (maxLon - minLon < math.pi * 2) { if (maxLon - minLon < math.pi * 2) {
if (lon + longitudeRad < minLon || lon + longitudeRad > maxLon) { if (lon + longitudeRad < minLon || lon + longitudeRad > maxLon) {
longitudeRad = (lon + longitudeRad < minLon ? minLon : maxLon) - lon; longitudeRad = (lon + longitudeRad < minLon ? minLon : maxLon) - lon;
// reverse rotation when reaching the boundary
if (widget.animSpeed != 0) {
if (widget.animReverse)
_animateDirection *= -1.0;
else
_controller.stop();
}
} }
} }
o.x = lon; o.x = lon;
@ -257,17 +267,17 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
q = orientationToQuaternion(o); q = orientationToQuaternion(o);
// rotate to longitude zero // rotate to longitude zero
q *= Quaternion.axisAngle(Vector3(0, 1, 0), -math.pi * 0.5); q *= Quaternion.axisAngle(Vector3(0, 1, 0), -_halfPi);
// rotate around the global Y axis // rotate around the global Y axis
q *= Quaternion.axisAngle(Vector3(0, 1, 0), longitudeRad); q *= Quaternion.axisAngle(Vector3(0, 1, 0), longitudeRad);
// rotate around the local X axis // rotate around the local X axis
q = Quaternion.axisAngle(Vector3(1, 0, 0), -latitudeRad) * q; q = Quaternion.axisAngle(Vector3(1, 0, 0), -latitudeRad) * q;
o = quaternionToOrientation(q * Quaternion.axisAngle(Vector3(0, 1, 0), math.pi * 0.5)); o = quaternionToOrientation(q * Quaternion.axisAngle(Vector3(0, 1, 0), _halfPi));
widget.onViewChanged?.call(degrees(o.x), degrees(-o.y), degrees(o.z)); widget.onViewChanged?.call(degrees(o.x), degrees(-o.y), degrees(o.z));
q.rotate(scene!.camera.target..setFrom(Vector3(0, 0, -_radius))); q.rotate(camera.target..setFrom(Vector3(0, 0, -_radius)));
q.rotate(scene!.camera.up..setFrom(Vector3(0, 1, 0))); q.rotate(camera.up..setFrom(Vector3(0, 1, 0)));
scene!.update(); scene!.update();
_streamController.add(null); _streamController.add(null);
} }
@ -318,13 +328,21 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
void _onSceneCreated(Scene scene) { void _onSceneCreated(Scene scene) {
this.scene = scene; this.scene = scene;
scene.camera.near = 1.0; final camera = scene.camera;
scene.camera.far = _radius + 1.0; camera.near = 1.0;
scene.camera.fov = 75; camera.far = _radius + 1.0;
scene.camera.zoom = widget.zoom; camera.fov = 75;
scene.camera.position.setFrom(Vector3(0, 0, 0.1)); camera.zoom = widget.zoom;
camera.position.setFrom(Vector3(0, 0, 0.1));
if (widget.child != null) { if (widget.child != null) {
final Mesh mesh = generateSphereMesh(radius: _radius, latSegments: widget.latSegments, lonSegments: widget.lonSegments, croppedArea: widget.croppedArea, croppedFullWidth: widget.croppedFullWidth, croppedFullHeight: widget.croppedFullHeight); final Mesh mesh = generateSphereMesh(
radius: _radius,
latSegments: widget.latSegments,
lonSegments: widget.lonSegments,
croppedArea: widget.croppedArea,
croppedFullWidth: widget.croppedFullWidth,
croppedFullHeight: widget.croppedFullHeight,
);
surface = Object(name: 'surface', mesh: mesh, backfaceCulling: false); surface = Object(name: 'surface', mesh: mesh, backfaceCulling: false);
_loadTexture(widget.child!.image); _loadTexture(widget.child!.image);
scene.world.add(surface!); scene.world.add(surface!);
@ -337,48 +355,52 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
} }
Vector3 positionToLatLon(double x, double y) { Vector3 positionToLatLon(double x, double y) {
final camera = scene!.camera;
// transform viewport coordinate to NDC, values between -1 and 1 // transform viewport coordinate to NDC, values between -1 and 1
final Vector4 v = Vector4(2.0 * x / scene!.camera.viewportWidth - 1.0, 1.0 - 2.0 * y / scene!.camera.viewportHeight, 1.0, 1.0); final v = Vector4(2.0 * x / camera.viewportWidth - 1.0, 1.0 - 2.0 * y / camera.viewportHeight, 1.0, 1.0);
// create projection matrix // create projection matrix
final Matrix4 m = scene!.camera.projectionMatrix * scene!.camera.lookAtMatrix; final m = camera.projectionMatrix * camera.lookAtMatrix;
// apply inversed projection matrix // apply inversed projection matrix
m.invert(); m.invert();
v.applyMatrix4(m); v.applyMatrix4(m);
// apply perspective division // apply perspective division
v.scale(1 / v.w); v.scale(1 / v.w);
// get rotation from two vectors // get rotation from two vectors
final Quaternion q = Quaternion.fromTwoVectors(v.xyz, Vector3(0.0, 0.0, -_radius)); final q = Quaternion.fromTwoVectors(v.xyz, Vector3(0.0, 0.0, -_radius));
// get euler angles from rotation // get euler angles from rotation
return quaternionToOrientation(q * Quaternion.axisAngle(Vector3(0, 1, 0), math.pi * 0.5)); return quaternionToOrientation(q * Quaternion.axisAngle(Vector3(0, 1, 0), _halfPi));
} }
Vector3 positionFromLatLon(double lat, double lon) { Vector3 positionFromLatLon(double lat, double lon) {
final camera = scene!.camera;
// create projection matrix // create projection matrix
final Matrix4 m = scene!.camera.projectionMatrix * scene!.camera.lookAtMatrix * matrixFromLatLon(lat, lon); final Matrix4 m = camera.projectionMatrix * camera.lookAtMatrix * matrixFromLatLon(lat, lon);
// apply projection matrix // apply projection matrix
final Vector4 v = Vector4(0.0, 0.0, -_radius, 1.0)..applyMatrix4(m); final Vector4 v = Vector4(0.0, 0.0, -_radius, 1.0)..applyMatrix4(m);
// apply perspective division and transform NDC to the viewport coordinate // apply perspective division and transform NDC to the viewport coordinate
return Vector3( return Vector3(
(1.0 + v.x / v.w) * scene!.camera.viewportWidth / 2, (1.0 + v.x / v.w) * camera.viewportWidth / 2,
(1.0 - v.y / v.w) * scene!.camera.viewportHeight / 2, (1.0 - v.y / v.w) * camera.viewportHeight / 2,
v.z, v.z,
); );
} }
Widget buildHotspotWidgets(List<Hotspot>? hotspots) { Widget buildHotspotWidgets(List<Hotspot>? hotspots) {
final List<Widget> widgets = <Widget>[]; final widgets = <Widget>[];
if (hotspots != null && scene != null) { if (hotspots != null && scene != null) {
for (Hotspot hotspot in hotspots) { for (Hotspot hotspot in hotspots) {
final Vector3 pos = positionFromLatLon(hotspot.latitude, hotspot.longitude); final pos = positionFromLatLon(hotspot.latitude, hotspot.longitude);
final Offset orgin = Offset(hotspot.width * hotspot.orgin.dx, hotspot.height * hotspot.orgin.dy); final origin = Offset(hotspot.width * hotspot.orgin.dx, hotspot.height * hotspot.orgin.dy);
final Matrix4 transform = scene!.camera.lookAtMatrix * matrixFromLatLon(hotspot.latitude, hotspot.longitude); final transform = scene!.camera.lookAtMatrix * matrixFromLatLon(hotspot.latitude, hotspot.longitude);
final Widget child = Positioned( final child = Positioned(
left: pos.x - orgin.dx, left: pos.x - origin.dx,
top: pos.y - orgin.dy, top: pos.y - origin.dy,
width: hotspot.width, width: hotspot.width,
height: hotspot.height, height: hotspot.height,
child: Transform( child: Transform(
origin: orgin, origin: origin,
transform: transform..invert(), transform: transform..invert(),
child: Offstage( child: Offstage(
offstage: pos.z < 0, offstage: pos.z < 0,
@ -403,7 +425,7 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
_updateSensorControl(); _updateSensorControl();
_controller = AnimationController(duration: Duration(milliseconds: 60000), vsync: this)..addListener(_updateView); _controller = AnimationController(duration: Duration(milliseconds: 60000), vsync: this)..addListener(_updateView);
if (widget.sensorControl != SensorControl.None || widget.animSpeed != 0) _controller.repeat(); if (widget.sensorControl != SensorControl.None) _controller.repeat();
} }
@override @override
@ -449,6 +471,7 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
? GestureDetector( ? GestureDetector(
onScaleStart: _handleScaleStart, onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate, onScaleUpdate: _handleScaleUpdate,
onScaleEnd: _handleScaleEnd,
onTapUp: widget.onTap == null ? null : _handleTapUp, onTapUp: widget.onTap == null ? null : _handleTapUp,
onLongPressStart: widget.onLongPressStart == null ? null : _handleLongPressStart, onLongPressStart: widget.onLongPressStart == null ? null : _handleLongPressStart,
onLongPressMoveUpdate: widget.onLongPressMoveUpdate == null ? null : _handleLongPressMoveUpdate, onLongPressMoveUpdate: widget.onLongPressMoveUpdate == null ? null : _handleLongPressMoveUpdate,