Skip to content

Instantly share code, notes, and snippets.

@filiph
Last active February 17, 2020 02:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save filiph/9359148ad9868540b7e3b686a3a232cd to your computer and use it in GitHub Desktop.
Save filiph/9359148ad9868540b7e3b686a3a232cd to your computer and use it in GitHub Desktop.
An experiment of spacial navigation
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MapPainter extends CustomPainter {
final Set<_Vector2> _points;
final _Vector2 _selectedPoint;
MapPainter(this._points, this._selectedPoint);
@override
void paint(Canvas canvas, Size size) {
Offset toOffset(_Vector2 point) =>
Offset(point.x * size.width, point.y * size.height);
final paint = Paint();
for (final point in _points) {
assert(point.x <= 1);
assert(point.y <= 1);
canvas.drawCircle(toOffset(point), 2, paint);
}
final redPaint = Paint()..color = Colors.red;
if (_selectedPoint != null) {
canvas.drawCircle(toOffset(_selectedPoint), 5, redPaint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Spacial Navigation Experiment'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static final _left = _Vector2(-1, 0);
static final _right = _Vector2(1, 0);
static final _up = _Vector2(0, -1);
static final _down = _Vector2(0, 1);
FocusNode _focusNode;
Set<_Vector2> _points = _generateVectors().toSet();
_Vector2 _selectedPoint;
String _displayArrow = '';
@override
Widget build(BuildContext context) {
return RawKeyboardListener(
autofocus: true,
focusNode: _focusNode,
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Align(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Tap here, then use hardware arrows.'),
SizedBox(height: 50),
CustomPaint(
size: Size(200, 200),
painter: MapPainter(_points, _selectedPoint),
),
SizedBox(height: 50),
SizedBox(
height: 50,
child: Text(_displayArrow, style: TextStyle(fontSize: 40)),
),
],
),
),
),
);
}
@override
void initState() {
super.initState();
_focusNode = FocusNode(onKey: _onKey);
_selectedPoint = _points.first;
}
void _moveInDirection(_Vector2 direction) {
final sorted = _listByClosest(_selectedPoint, direction, _points);
final nextSelected = sorted.firstWhere((v) {
// Only take points that are in the general direction.
return (v - _selectedPoint).angleTo(direction) < pi / 2;
}, orElse: () => _selectedPoint);
setState(() {
_selectedPoint = nextSelected;
});
}
bool _onKey(FocusNode node, RawKeyEvent event) {
if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
_moveInDirection(_left);
_displayArrow = '⇦';
return true;
}
if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
_moveInDirection(_right);
_displayArrow = '⇨';
return true;
}
if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) {
_moveInDirection(_up);
_displayArrow = '⇧';
return true;
}
if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) {
_moveInDirection(_down);
_displayArrow = '⇩';
return true;
}
return false;
}
static Iterable<_Vector2> _generateVectors() sync* {
var random = Random();
for (int i = 0; i < 20; i++) {
yield _Vector2(random.nextDouble(), random.nextDouble());
}
}
/// Returns [points] sorted by closeness "score". Points that are close
/// and in the [direction] from [origin] will be ranked higher.
///
/// This list will still have points that might not be a good fit.
/// Those can be filtered out by the caller of the method.
static List<_Vector2> _listByClosest(
_Vector2 origin, _Vector2 direction, Set<_Vector2> points) {
final result = List<_Vector2>.from(points);
result.remove(origin);
result.sort((_Vector2 a, _Vector2 b) {
final aDistance = (a.x - origin.x).abs() + (a.y - origin.y).abs();
if (aDistance == 0) return 1;
final bDistance = (b.x - origin.x).abs() + (b.y - origin.y).abs();
if (bDistance == 0) return -1;
final aDistNormalized = aDistance / max(aDistance, bDistance);
final bDistNormalized = bDistance / max(aDistance, bDistance);
final aAngle = (a - origin).angleTo(direction);
final bAngle = (b - origin).angleTo(direction);
final aAngleNormalized = min(aAngle, pi / 3) / (pi / 3);
final bAngleNormalized = min(bAngle, pi / 3) / (pi / 3);
final aScore = aDistNormalized + aAngleNormalized;
final bScore = bDistNormalized + bAngleNormalized;
return aScore.compareTo(bScore);
});
return result;
}
}
/// A simple immutable 2D vector class.
class _Vector2 {
final double x, y;
const _Vector2(this.x, this.y);
/// TO DO: better hash code
@override
int get hashCode => x.hashCode ^ (y.hashCode + 42);
/// Length.
double get length => sqrt(length2);
/// Length squared.
double get length2 {
double sum;
sum = (x * x);
sum += (y * y);
return sum;
}
/// Add two vectors.
_Vector2 operator +(_Vector2 other) => _Vector2(x + other.x, y + other.y);
/// Negate.
_Vector2 operator -() => _Vector2(-x, -y);
/// Subtract two vectors.
_Vector2 operator -(_Vector2 other) => _Vector2(x - other.x, y - other.y);
/// Check if two vectors are the same.
@override
bool operator ==(Object other) =>
(other is _Vector2) && (x == other.x) && (y == other.y);
/// Returns the angle between [this] vector and [other] in radians.
double angleTo(_Vector2 other) {
if (x == other.x && y == other.y) {
return 0.0;
}
final double d = dot(other) / (length * other.length);
return acos(d.clamp(-1.0, 1.0));
}
/// Returns the signed angle between [this] and [other] in radians.
double angleToSigned(_Vector2 other) {
if (x == other.x && y == other.y) {
return 0.0;
}
final double s = cross(other);
final double c = dot(other);
return atan2(s, c);
}
/// Cross product.
double cross(_Vector2 other) {
return x * other.y - y * other.x;
}
/// Inner product.
double dot(_Vector2 other) {
return x * other.x + y * other.y;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment