diff --git a/lib/panorama.dart b/lib/panorama.dart index a82d87d..b1e36b5 100644 --- a/lib/panorama.dart +++ b/lib/panorama.dart @@ -114,14 +114,17 @@ class Panorama extends StatefulWidget { final Function(double longitude, double latitude, double tilt)? onTap; /// This event will be called when the user has started a long press, it contains latitude and longitude about where the user pressed. - final Function(double longitude, double latitude, double tilt)? onLongPressStart; + final Function(double longitude, double latitude, double tilt)? + onLongPressStart; /// This event will be called when the user has drag-moved after a long press, it contains latitude and longitude about where the user pressed. - final Function(double longitude, double latitude, double tilt)? onLongPressMoveUpdate; + final Function(double longitude, double latitude, double tilt)? + onLongPressMoveUpdate; /// 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. final Function()? onImageLoad; @@ -135,7 +138,8 @@ class Panorama extends StatefulWidget { _PanoramaState createState() => _PanoramaState(); } -class _PanoramaState extends State with SingleTickerProviderStateMixin { +class _PanoramaState extends State + with SingleTickerProviderStateMixin { Scene? scene; Object? surface; late double latitude; @@ -158,22 +162,26 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin ImageStream? _imageStream; void _handleTapUp(TapUpDetails details) { - final Vector3 o = positionToLatLon(details.localPosition.dx, details.localPosition.dy); + final Vector3 o = + positionToLatLon(details.localPosition.dx, details.localPosition.dy); widget.onTap!(degrees(o.x), degrees(-o.y), degrees(o.z)); } void _handleLongPressStart(LongPressStartDetails details) { - final Vector3 o = positionToLatLon(details.localPosition.dx, details.localPosition.dy); + final Vector3 o = + positionToLatLon(details.localPosition.dx, details.localPosition.dy); widget.onLongPressStart!(degrees(o.x), degrees(-o.y), degrees(o.z)); } void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - final Vector3 o = positionToLatLon(details.localPosition.dx, details.localPosition.dy); + final Vector3 o = + positionToLatLon(details.localPosition.dx, details.localPosition.dy); widget.onLongPressMoveUpdate!(degrees(o.x), degrees(-o.y), degrees(o.z)); } void _handleLongPressEnd(LongPressEndDetails details) { - final Vector3 o = positionToLatLon(details.localPosition.dx, details.localPosition.dy); + final Vector3 o = + positionToLatLon(details.localPosition.dx, details.localPosition.dy); widget.onLongPressEnd!(degrees(o.x), degrees(-o.y), degrees(o.z)); } @@ -185,13 +193,23 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin 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; + 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 (widget.sensorControl == SensorControl.None && !_controller.isAnimating) { + if (widget.sensorControl == SensorControl.None && + !_controller.isAnimating) { _controller.reset(); if (widget.animSpeed != 0) { _controller.repeat(); @@ -208,7 +226,10 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin latitude += latitudeDelta * _dampingFactor * widget.sensitivity; latitudeDelta *= 1 - _dampingFactor * widget.sensitivity; // animate horizontal rotating - longitude += _animateDirection * longitudeDelta * _dampingFactor * widget.sensitivity; + longitude += _animateDirection * + longitudeDelta * + _dampingFactor * + widget.sensitivity; longitudeDelta *= 1 - _dampingFactor * widget.sensitivity; // animate zomming final double zoom = scene!.camera.zoom + zoomDelta * _dampingFactor; @@ -263,7 +284,8 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin // rotate around the local X axis q = Quaternion.axisAngle(Vector3(1, 0, 0), -latitude) * q; - o = quaternionToOrientation(q * Quaternion.axisAngle(Vector3(0, 1, 0), math.pi * 0.5)); + o = quaternionToOrientation( + q * Quaternion.axisAngle(Vector3(0, 1, 0), math.pi * 0.5)); widget.onViewChanged?.call(degrees(o.x), degrees(-o.y), degrees(o.z)); q.rotate(scene!.camera.target..setFrom(Vector3(0, 0, -_radius))); @@ -276,14 +298,18 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin _orientationSubscription?.cancel(); switch (widget.sensorControl) { case SensorControl.Orientation: - motionSensors.orientationUpdateInterval = Duration.microsecondsPerSecond ~/ 60; - _orientationSubscription = motionSensors.orientation.listen((OrientationEvent event) { + motionSensors.orientationUpdateInterval = + Duration.microsecondsPerSecond ~/ 60; + _orientationSubscription = + motionSensors.orientation.listen((OrientationEvent event) { orientation.setValues(event.yaw, event.pitch, event.roll); }); break; case SensorControl.AbsoluteOrientation: - motionSensors.absoluteOrientationUpdateInterval = Duration.microsecondsPerSecond ~/ 60; - _orientationSubscription = motionSensors.absoluteOrientation.listen((AbsoluteOrientationEvent event) { + motionSensors.absoluteOrientationUpdateInterval = + Duration.microsecondsPerSecond ~/ 60; + _orientationSubscription = motionSensors.absoluteOrientation + .listen((AbsoluteOrientationEvent event) { orientation.setValues(event.yaw, event.pitch, event.roll); }); break; @@ -292,7 +318,8 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin _screenOrientSubscription?.cancel(); if (widget.sensorControl != SensorControl.None) { - _screenOrientSubscription = motionSensors.screenOrientation.listen((ScreenOrientationEvent event) { + _screenOrientSubscription = motionSensors.screenOrientation + .listen((ScreenOrientationEvent event) { screenOrientation = radians(event.angle!); }); } @@ -300,7 +327,8 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin void _updateTexture(ImageInfo imageInfo, bool synchronousCall) { surface?.mesh.texture = imageInfo.image; - surface?.mesh.textureRect = Rect.fromLTWH(0, 0, imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble()); + surface?.mesh.textureRect = Rect.fromLTWH(0, 0, + imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble()); scene!.texture = imageInfo.image; scene!.update(); widget.onImageLoad?.call(); @@ -312,6 +340,9 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin _imageStream = provider.resolve(ImageConfiguration()); ImageStreamListener listener = ImageStreamListener(_updateTexture); _imageStream!.addListener(listener); + + latitude = degrees(widget.latitude); // Fix Position + longitude = degrees(widget.longitude); // Fix Position } void _onSceneCreated(Scene scene) { @@ -322,7 +353,13 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin scene.camera.zoom = widget.zoom; scene.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!); @@ -336,23 +373,29 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin Vector3 positionToLatLon(double x, double y) { // 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 Vector4 v = Vector4(2.0 * x / scene!.camera.viewportWidth - 1.0, + 1.0 - 2.0 * y / scene!.camera.viewportHeight, 1.0, 1.0); // create projection matrix - final Matrix4 m = scene!.camera.projectionMatrix * scene!.camera.lookAtMatrix; + final Matrix4 m = + scene!.camera.projectionMatrix * scene!.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 Quaternion 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), math.pi * 0.5)); } Vector3 positionFromLatLon(double lat, double lon) { // create projection matrix - final Matrix4 m = scene!.camera.projectionMatrix * scene!.camera.lookAtMatrix * matrixFromLatLon(lat, lon); + final Matrix4 m = scene!.camera.projectionMatrix * + scene!.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 @@ -367,9 +410,12 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin final List widgets = []; 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 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, @@ -400,8 +446,11 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin _updateSensorControl(); - _controller = AnimationController(duration: Duration(milliseconds: 60000), vsync: this)..addListener(_updateView); - if (widget.sensorControl != SensorControl.None || widget.animSpeed != 0) _controller.repeat(); + _controller = AnimationController( + duration: Duration(milliseconds: 60000), vsync: this) + ..addListener(_updateView); + if (widget.sensorControl != SensorControl.None || widget.animSpeed != 0) + _controller.repeat(); } @override @@ -418,8 +467,18 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin void didUpdateWidget(Panorama oldWidget) { super.didUpdateWidget(oldWidget); if (surface == null) return; - if (widget.latSegments != oldWidget.latSegments || widget.lonSegments != oldWidget.lonSegments || widget.croppedArea != oldWidget.croppedArea || widget.croppedFullWidth != oldWidget.croppedFullWidth || widget.croppedFullHeight != oldWidget.croppedFullHeight) { - surface!.mesh = generateSphereMesh(radius: _radius, latSegments: widget.latSegments, lonSegments: widget.lonSegments, croppedArea: widget.croppedArea, croppedFullWidth: widget.croppedFullWidth, croppedFullHeight: widget.croppedFullHeight); + if (widget.latSegments != oldWidget.latSegments || + widget.lonSegments != oldWidget.lonSegments || + widget.croppedArea != oldWidget.croppedArea || + widget.croppedFullWidth != oldWidget.croppedFullWidth || + widget.croppedFullHeight != oldWidget.croppedFullHeight) { + surface!.mesh = generateSphereMesh( + radius: _radius, + latSegments: widget.latSegments, + lonSegments: widget.lonSegments, + croppedArea: widget.croppedArea, + croppedFullWidth: widget.croppedFullWidth, + croppedFullHeight: widget.croppedFullHeight); } if (widget.child?.image != oldWidget.child?.image) { _loadTexture(widget.child?.image); @@ -448,9 +507,13 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin onScaleStart: _handleScaleStart, onScaleUpdate: _handleScaleUpdate, onTapUp: widget.onTap == null ? null : _handleTapUp, - onLongPressStart: widget.onLongPressStart == null ? null : _handleLongPressStart, - onLongPressMoveUpdate: widget.onLongPressMoveUpdate == null ? null : _handleLongPressMoveUpdate, - onLongPressEnd: widget.onLongPressEnd == null ? null : _handleLongPressEnd, + onLongPressStart: + widget.onLongPressStart == null ? null : _handleLongPressStart, + onLongPressMoveUpdate: widget.onLongPressMoveUpdate == null + ? null + : _handleLongPressMoveUpdate, + onLongPressEnd: + widget.onLongPressEnd == null ? null : _handleLongPressEnd, child: pano, ) : pano; @@ -489,22 +552,33 @@ class Hotspot { Widget? widget; } -Mesh generateSphereMesh({num radius = 1.0, int latSegments = 16, int lonSegments = 16, ui.Image? texture, Rect croppedArea = const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0), double croppedFullWidth = 1.0, double croppedFullHeight = 1.0}) { +Mesh generateSphereMesh( + {num radius = 1.0, + int latSegments = 16, + int lonSegments = 16, + ui.Image? texture, + Rect croppedArea = const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0), + double croppedFullWidth = 1.0, + double croppedFullHeight = 1.0}) { int count = (latSegments + 1) * (lonSegments + 1); List vertices = List.filled(count, Vector3.zero()); List texcoords = List.filled(count, Offset.zero); - List indices = List.filled(latSegments * lonSegments * 2, Polygon(0, 0, 0)); + List indices = + List.filled(latSegments * lonSegments * 2, Polygon(0, 0, 0)); int i = 0; for (int y = 0; y <= latSegments; ++y) { final double tv = y / latSegments; - final double v = (croppedArea.top + croppedArea.height * tv) / croppedFullHeight; + final double v = + (croppedArea.top + croppedArea.height * tv) / croppedFullHeight; 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 tu = x / lonSegments; - final double u = (croppedArea.left + croppedArea.width * tu) / croppedFullWidth; - vertices[i] = Vector3(radius * math.cos(u * math.pi * 2.0) * sv, radius * cv, radius * math.sin(u * math.pi * 2.0) * sv); + final double u = + (croppedArea.left + croppedArea.width * tu) / croppedFullWidth; + 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(tu, 1.0 - tv); i++; } @@ -520,7 +594,11 @@ Mesh generateSphereMesh({num radius = 1.0, int latSegments = 16, int lonSegments } } - final Mesh mesh = Mesh(vertices: vertices, texcoords: texcoords, indices: indices, texture: texture); + final Mesh mesh = Mesh( + vertices: vertices, + texcoords: texcoords, + indices: indices, + texture: texture); return mesh; } @@ -533,9 +611,11 @@ Vector3 quaternionToOrientation(Quaternion q) { final double y = storage[1]; final double z = storage[2]; final double w = storage[3]; - final double roll = math.atan2(-2 * (x * y - w * z), 1.0 - 2 * (x * x + z * z)); + final double roll = + math.atan2(-2 * (x * y - w * z), 1.0 - 2 * (x * x + z * z)); final double pitch = math.asin(2 * (y * z + w * x)); - final double yaw = math.atan2(-2 * (x * z - w * y), 1.0 - 2 * (x * x + y * y)); + final double yaw = + math.atan2(-2 * (x * z - w * y), 1.0 - 2 * (x * x + y * y)); return Vector3(yaw, pitch, roll); }