Skip to content

Instantly share code, notes, and snippets.

@roipeker
Created June 4, 2021 16:38
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save roipeker/000fee572fe2f575774d39aad4965772 to your computer and use it in GitHub Desktop.
Save roipeker/000fee572fe2f575774d39aad4965772 to your computer and use it in GitHub Desktop.
GraphX issue #19: gesture detector sample (zoom/pan/rotation with easing)
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),
),
),
);
}
}
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