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

View file

@ -1,8 +1,9 @@
library panorama;
import 'dart:async';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_cube/flutter_cube.dart';
import 'package:motion_sensors/motion_sensors.dart';
@ -30,8 +31,7 @@ class Panorama extends StatefulWidget {
this.maxLongitude = 180.0,
this.minZoom = 1.0,
this.maxZoom = 5.0,
this.sensitivity = 1.0,
this.animSpeed = 0.0,
this.panInertia = 0.05,
this.animReverse = true,
this.latSegments = 32,
this.lonSegments = 64,
@ -77,11 +77,8 @@ class Panorama extends StatefulWidget {
/// 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 0.0
final double animSpeed;
/// default to 0.05
final double panInertia;
/// Reverse rotation when the current longitude reaches the minimal or maximum. default to true
final bool animReverse;
@ -145,8 +142,6 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
double zoomDelta = 0;
late Offset _lastFocalPoint;
double? _lastZoom;
double _radius = 500;
double _dampingFactor = 0.05;
double _animateDirection = 1.0;
late AnimationController _controller;
double screenOrientationRad = 0.0;
@ -156,6 +151,12 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
late StreamController<Null> _streamController;
Stream<Null>? _stream;
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) {
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) {
_lastFocalPoint = details.localFocalPoint;
_lastZoom = null;
_scaling = true;
}
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);
_updatePositionDeltaForOffset(offset);
final zoom = scene!.camera.zoom;
_lastZoom ??= zoom;
zoomDelta += _lastZoom! * details.scale - (zoom + zoomDelta);
if (widget.sensorControl == SensorControl.None && !_controller.isAnimating) {
_controller.reset();
if (widget.animSpeed != 0) {
_controller.repeat();
} else
_controller.forward();
_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() {
if (scene == null) return;
// auto rotate
longitudeDelta += 0.001 * widget.animSpeed;
final camera = scene!.camera;
final damping = _scaling ? _panReactivity : widget.panInertia;
// animate vertical rotating
latitudeRad += latitudeDelta * _dampingFactor * widget.sensitivity;
latitudeDelta *= 1 - _dampingFactor * widget.sensitivity;
latitudeRad += latitudeDelta * damping;
latitudeDelta *= 1 - damping;
// animate horizontal rotating
longitudeRad += _animateDirection * longitudeDelta * _dampingFactor * widget.sensitivity;
longitudeDelta *= 1 - _dampingFactor * widget.sensitivity;
// animate zomming
final double zoom = scene!.camera.zoom + zoomDelta * _dampingFactor;
zoomDelta *= 1 - _dampingFactor;
scene!.camera.zoom = zoom.clamp(widget.minZoom, widget.maxZoom);
longitudeRad += _animateDirection * longitudeDelta * damping;
longitudeDelta *= 1 - damping;
// animate zooming
final double zoom = camera.zoom + zoomDelta * damping;
zoomDelta *= 1 - damping;
camera.zoom = zoom.clamp(widget.minZoom, widget.maxZoom);
// stop animation if not needed
if (latitudeDelta.abs() < 0.001 &&
longitudeDelta.abs() < 0.001 &&
zoomDelta.abs() < 0.001) {
if (widget.sensorControl == SensorControl.None &&
widget.animSpeed == 0 &&
_controller.isAnimating) _controller.stop();
if (latitudeDelta.abs() < _epsilon && longitudeDelta.abs() < _epsilon && zoomDelta.abs() < _epsilon) {
if (widget.sensorControl == SensorControl.None && _controller.isAnimating) {
_controller.stop();
}
}
// rotate for screen orientation
@ -228,7 +245,7 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
// rotate for device orientation
q *= Quaternion.euler(-orientation.z, -orientation.y, -orientation.x);
// 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
Vector3 o = quaternionToOrientation(q);
@ -243,13 +260,6 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
if (maxLon - minLon < math.pi * 2) {
if (lon + longitudeRad < minLon || lon + longitudeRad > maxLon) {
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;
@ -257,17 +267,17 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
q = orientationToQuaternion(o);
// 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
q *= Quaternion.axisAngle(Vector3(0, 1, 0), longitudeRad);
// rotate around the local X axis
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));
q.rotate(scene!.camera.target..setFrom(Vector3(0, 0, -_radius)));
q.rotate(scene!.camera.up..setFrom(Vector3(0, 1, 0)));
q.rotate(camera.target..setFrom(Vector3(0, 0, -_radius)));
q.rotate(camera.up..setFrom(Vector3(0, 1, 0)));
scene!.update();
_streamController.add(null);
}
@ -318,13 +328,21 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
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));
final camera = scene.camera;
camera.near = 1.0;
camera.far = _radius + 1.0;
camera.fov = 75;
camera.zoom = widget.zoom;
camera.position.setFrom(Vector3(0, 0, 0.1));
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);
_loadTexture(widget.child!.image);
scene.world.add(surface!);
@ -337,48 +355,52 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
}
Vector3 positionToLatLon(double x, double y) {
final camera = scene!.camera;
// 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
final Matrix4 m = scene!.camera.projectionMatrix * scene!.camera.lookAtMatrix;
final m = camera.projectionMatrix * camera.lookAtMatrix;
// apply inversed projection matrix
m.invert();
v.applyMatrix4(m);
// apply perspective division
v.scale(1 / v.w);
// 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
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) {
final camera = scene!.camera;
// 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
final Vector4 v = Vector4(0.0, 0.0, -_radius, 1.0)..applyMatrix4(m);
// apply perspective division and transform NDC to the viewport coordinate
return Vector3(
(1.0 + v.x / v.w) * scene!.camera.viewportWidth / 2,
(1.0 - v.y / v.w) * scene!.camera.viewportHeight / 2,
(1.0 + v.x / v.w) * camera.viewportWidth / 2,
(1.0 - v.y / v.w) * camera.viewportHeight / 2,
v.z,
);
}
Widget buildHotspotWidgets(List<Hotspot>? hotspots) {
final List<Widget> widgets = <Widget>[];
final widgets = <Widget>[];
if (hotspots != null && scene != null) {
for (Hotspot hotspot in hotspots) {
final Vector3 pos = positionFromLatLon(hotspot.latitude, hotspot.longitude);
final Offset orgin = Offset(hotspot.width * hotspot.orgin.dx, hotspot.height * hotspot.orgin.dy);
final Matrix4 transform = scene!.camera.lookAtMatrix * matrixFromLatLon(hotspot.latitude, hotspot.longitude);
final Widget child = Positioned(
left: pos.x - orgin.dx,
top: pos.y - orgin.dy,
final pos = positionFromLatLon(hotspot.latitude, hotspot.longitude);
final origin = Offset(hotspot.width * hotspot.orgin.dx, hotspot.height * hotspot.orgin.dy);
final transform = scene!.camera.lookAtMatrix * matrixFromLatLon(hotspot.latitude, hotspot.longitude);
final child = Positioned(
left: pos.x - origin.dx,
top: pos.y - origin.dy,
width: hotspot.width,
height: hotspot.height,
child: Transform(
origin: orgin,
origin: origin,
transform: transform..invert(),
child: Offstage(
offstage: pos.z < 0,
@ -403,7 +425,7 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
_updateSensorControl();
_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
@ -449,6 +471,7 @@ class _PanoramaState extends State<Panorama> with SingleTickerProviderStateMixin
? GestureDetector(
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
onScaleEnd: _handleScaleEnd,
onTapUp: widget.onTap == null ? null : _handleTapUp,
onLongPressStart: widget.onLongPressStart == null ? null : _handleLongPressStart,
onLongPressMoveUpdate: widget.onLongPressMoveUpdate == null ? null : _handleLongPressMoveUpdate,