Created
June 4, 2021 16:38
-
-
Save roipeker/000fee572fe2f575774d39aad4965772 to your computer and use it in GitHub Desktop.
GraphX issue #19: gesture detector sample (zoom/pan/rotation with easing)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:graphx/graphx.dart'; | |
import 'zoom_scene.dart'; | |
/// Live demo: | |
/// https://graphx-gesture-sample.surge.sh | |
/// | |
void main() { | |
runApp(MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData.dark(), | |
home: Scaffold( | |
appBar: AppBar( | |
title: Text( | |
'graphx pan + zoom + rotation', | |
style: TextStyle(color: Colors.white60, fontSize: 12), | |
), | |
), | |
body: GestureDetector( | |
onScaleStart: (e) => mps.emit1(ZoomEvent.scaleStart, e), | |
onScaleUpdate: (e) => mps.emit1(ZoomEvent.scaleUpdate, e), | |
onScaleEnd: (e) => mps.emit1(ZoomEvent.scaleEnd, e), | |
child: SceneBuilderWidget( | |
builder: () => SceneController( | |
front: ZoomScene(), | |
config: SceneConfig.tools, | |
), | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: () => mps.emit(ZoomEvent.scaleReset), | |
tooltip: 'reset', | |
child: Icon(Icons.aspect_ratio), | |
), | |
), | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:graphx/graphx.dart'; | |
/// **** GRAPHX CODE **** | |
abstract class ZoomEvent { | |
static const scaleUpdate = 'scaleUpdate'; | |
static const scaleStart = 'scaleStart'; | |
static const scaleEnd = 'scaleEnd'; | |
static const scaleReset = 'scaleReset'; | |
} | |
class ZoomScene extends GSprite { | |
late GSprite content; | |
late GShape bg; | |
GPoint targetPosition = GPoint(), | |
grabPoint = GPoint(), | |
dragPoint = GPoint(), | |
_velocity = GPoint(); | |
bool isDragging = false; | |
double currScale = 1.0; | |
double targetScale = 1.0; | |
double velFriction = .92; | |
double dragEasing = 4; | |
bool isZoomReset = false; | |
/// track rotation. | |
double prevRot = 0.0, targetRot = 0.0; | |
/// create background texture. | |
static GTexture? _bgTexture; | |
static Future<void> _makeBackgroundPattern() async { | |
var shape = GShape(); | |
shape.graphics | |
.beginFill(Colors.grey.shade900) | |
.drawRect(0, 0, 30, 30) | |
.endFill(); | |
shape.graphics | |
.beginFill(Colors.white.withOpacity(.12)) | |
.drawCircle(1, 1, 1) | |
.endFill(); | |
_bgTexture = await shape.createImageTexture(true, 1); | |
} | |
GPoint _getRandomPos([GPoint? output]) { | |
output ??= GPoint(); | |
var offset = 200.0; | |
var maxX = (stage?.stageWidth ?? 0) + offset * 2; | |
var maxY = (stage?.stageHeight ?? 0) + offset * 2; | |
output.setTo( | |
Math.randomRange(-offset, maxX), | |
Math.randomRange(-offset, maxY), | |
); | |
return output; | |
} | |
@override | |
void addedToStage() { | |
super.addedToStage(); | |
stage!.color = Colors.grey.shade900; | |
stage!.maskBounds = true; | |
_makeBackgroundPattern(); | |
bg = GShape(); | |
content = GSprite(); | |
/// create a lot of quads. | |
List.generate(100, (index) { | |
var box = GShape(); | |
box.graphics | |
..beginFill(Math.randomList(Colors.primaries)) | |
..lineStyle(1, Colors.white70) | |
..drawRoundRect(0, 0, 30, 30, 6) | |
..endFill(); | |
GPoint pos = _getRandomPos(); | |
box.setPosition(pos.x, pos.y); | |
content.addChild(box); | |
/// make the boxes move forever. | |
void _tweenBox() { | |
pos = _getRandomPos(pos); | |
box.tween( | |
duration: Math.randomRange(.5, 2), | |
delay: Math.randomRange(.25, 1), | |
x: pos.x, | |
y: pos.y, | |
onComplete: _tweenBox, | |
); | |
} | |
_tweenBox(); | |
}); | |
addChild(bg); | |
addChild(content); | |
listenEvents(); | |
drawBackground(); | |
} | |
void _onScaleStart(ScaleStartDetails e) { | |
if (isZoomReset) return; | |
if (e.pointerCount == 1) { | |
isDragging = true; | |
_velocity.setEmpty(); | |
} | |
dragPoint = GPoint.fromNative(e.localFocalPoint); | |
var innerPoint = content.globalToLocal(dragPoint); | |
content.pivotX = innerPoint.x; | |
content.pivotY = innerPoint.y; | |
var canvasMouse = globalToLocal(dragPoint); | |
content.setPosition(canvasMouse.x, canvasMouse.y); | |
targetPosition.setTo(canvasMouse.x, canvasMouse.y); | |
grabPoint.setTo(content.x, content.y); | |
currScale = content.scale; | |
} | |
void _onScaleUpdate(ScaleUpdateDetails e) { | |
if (isZoomReset) return; | |
isDragging = true; | |
var currentPoint = GPoint.fromNative(e.localFocalPoint); | |
var dx = currentPoint.x - dragPoint.x; | |
var dy = currentPoint.y - dragPoint.y; | |
targetPosition.setTo(grabPoint.x + dx, grabPoint.y + dy); | |
targetScale = e.scale * currScale; | |
clampScale(); | |
if (e.rotation != 0) { | |
var diff = e.rotation - prevRot; | |
prevRot = e.rotation; | |
targetRot += diff; | |
if (diff >= Math.PI * 1.9) { | |
targetRot -= Math.PI * 2; | |
} else if (diff <= -Math.PI * 1.9) { | |
targetRot += Math.PI * 2; | |
} | |
} | |
} | |
void _onScaleEnd(ScaleEndDetails e) { | |
if (isZoomReset) return; | |
isDragging = false; | |
final vel = e.velocity; //.clampMagnitude(0.0, 500); | |
_velocity.setTo(vel.pixelsPerSecond.dx / 100, vel.pixelsPerSecond.dy / 100); | |
dragPoint.setEmpty(); | |
} | |
/// mouse wheel. | |
void _onMouseScroll(MouseInputData event) { | |
if (isZoomReset) return; | |
GPoint dragPoint = event.stagePosition; | |
var innerPoint = content.globalToLocal(dragPoint); | |
content.pivotX = innerPoint.x; | |
content.pivotY = innerPoint.y; | |
var canvasMouse = globalToLocal(dragPoint); | |
content.setPosition(canvasMouse.x, canvasMouse.y); | |
targetScale += -event.scrollDelta.y * .001; | |
clampScale(); | |
} | |
void _onStageResize() { | |
drawBackground(); | |
} | |
void _onUpdate(double event) { | |
if (isZoomReset) return; | |
var syncBg = false; | |
var zoomDistance = targetScale - content.scaleX; | |
if (zoomDistance.abs() > .001) { | |
content.scale += zoomDistance / 2; | |
syncBg = true; | |
} | |
/// rotation distance (only with multitouch) | |
var dr = targetRot - content.rotation; | |
if (dr.abs() > .001) { | |
content.rotation += dr / 6; | |
} | |
if (isDragging) { | |
var dx = targetPosition.x - content.x; | |
var dy = targetPosition.y - content.y; | |
if (dx.abs() > .1) { | |
content.x += dx / dragEasing; | |
syncBg = true; | |
} | |
if (dy.abs() > .1) { | |
content.y += dy / dragEasing; | |
syncBg = true; | |
} | |
} else { | |
if (_velocity.y != 0) { | |
_velocity.y *= velFriction; | |
content.y += _velocity.y; | |
if (_velocity.y.abs() < .1) { | |
_velocity.y = 0; | |
} else { | |
syncBg = true; | |
} | |
} | |
if (_velocity.x != 0) { | |
_velocity.x *= velFriction; | |
content.x += _velocity.x; | |
if (_velocity.x.abs() < .1) { | |
_velocity.x = 0; | |
} else { | |
syncBg = true; | |
} | |
} | |
} | |
if (syncBg) { | |
drawBackground(); | |
} | |
} | |
void listenEvents() { | |
mps.on(ZoomEvent.scaleStart, _onScaleStart); | |
mps.on(ZoomEvent.scaleUpdate, _onScaleUpdate); | |
mps.on(ZoomEvent.scaleEnd, _onScaleEnd); | |
mps.on(ZoomEvent.scaleReset, _resetZoom); | |
stage!.onEnterFrame.add(_onUpdate); | |
/// for Desktop... | |
stage!.onMouseScroll.add(_onMouseScroll); | |
stage!.keyboard!.onDown.add(_onKeyDown); | |
/// window resizes. | |
stage!.onResized.add(_onStageResize); | |
} | |
void _onKeyDown(KeyboardEventData e) { | |
if (e.isKey(LogicalKeyboardKey.keyR)) { | |
_resetZoom(); | |
} | |
} | |
@override | |
void dispose() { | |
mps.offAll(ZoomEvent.scaleStart); | |
mps.offAll(ZoomEvent.scaleUpdate); | |
mps.offAll(ZoomEvent.scaleEnd); | |
mps.offAll(ZoomEvent.scaleReset); | |
stage!.onEnterFrame.remove(_onUpdate); | |
stage!.onMouseScroll.remove(_onMouseScroll); | |
stage!.keyboard!.onDown.remove(_onKeyDown); | |
stage!.onResized.remove(_onStageResize); | |
super.dispose(); | |
} | |
void drawBackground() async { | |
if (_bgTexture == null) return; | |
final tx = _bgTexture!; | |
final matrix = content.transformationMatrix; | |
bg.graphics | |
.clear() | |
.beginBitmapFill(tx, matrix, true, false) | |
.drawRect(0, 0, stage!.stageWidth, stage!.stageHeight) | |
.endFill(); | |
} | |
void _resetZoom() { | |
/// reset most flags while Tweening. | |
isZoomReset = true; | |
_velocity.setEmpty(); | |
isDragging = false; | |
dragPoint.setEmpty(); | |
targetPosition.setEmpty(); | |
targetScale = 1.0; | |
content.tween( | |
duration: .8, | |
x: 0, | |
y: 0, | |
pivotX: 0, | |
pivotY: 0, | |
scale: 1, | |
ease: GEase.fastLinearToSlowEaseIn, | |
onComplete: () { | |
isZoomReset = false; | |
}, | |
onUpdate: () { | |
drawBackground(); | |
}); | |
} | |
void clampScale() { | |
targetScale = targetScale.clamp(.5, 3.0); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment