add scaling with hit test
This commit is contained in:
+19
-1
@@ -10,7 +10,7 @@ class App extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AppState extends State<App> {
|
class _AppState extends State<App> {
|
||||||
final CanvasController controller = CanvasController(debug: true);
|
final CanvasController controller = CanvasController(debug: true, initialScale: 1);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -24,6 +24,24 @@ class _AppState extends State<App> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
floatingActionButton: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.updateScalebyDelta(0.1);
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.zoom_in),
|
||||||
|
),
|
||||||
|
FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.updateScalebyDelta(-0.1);
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.zoom_out),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: CanvasView(controller: controller, canvasBackground: SingleColorBackround(Colors.white)),
|
body: CanvasView(controller: controller, canvasBackground: SingleColorBackround(Colors.white)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,12 @@ import 'package:example/app.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(MaterialApp(title: 'Infinite Lazy 2D Grid Example', home: const App(), debugShowCheckedModeBanner: false));
|
runApp(
|
||||||
|
MaterialApp(
|
||||||
|
// showPerformanceOverlay: true,
|
||||||
|
title: 'Infinite Lazy 2D Grid Example',
|
||||||
|
home: const App(),
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ abstract class CanvasBackground {
|
|||||||
|
|
||||||
/// draw the backround on this context. Implement this to have
|
/// draw the backround on this context. Implement this to have
|
||||||
/// different kinds of backgrounds
|
/// different kinds of backgrounds
|
||||||
void paint(Canvas canvas, Size canvasSize);
|
void paint(Canvas canvas, Size canvasSize, double scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoBackground extends CanvasBackground {
|
class NoBackground extends CanvasBackground {
|
||||||
const NoBackground();
|
const NoBackground();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size canvasSize) {}
|
void paint(Canvas canvas, Size canvasSize, double scale) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleColorBackround extends CanvasBackground {
|
class SingleColorBackround extends CanvasBackground {
|
||||||
const SingleColorBackround(Color backgroundColor) : super(bgColor: backgroundColor);
|
const SingleColorBackround(Color backgroundColor) : super(bgColor: backgroundColor);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size canvasSize) {
|
void paint(Canvas canvas, Size canvasSize, double scale) {
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
..color = bgColor
|
..color = bgColor
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
|
|||||||
+53
-15
@@ -12,11 +12,18 @@ typedef WidgetBuilder = Widget Function();
|
|||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
final _ChildNotFoundException = Exception('Child with the given ID does not exist');
|
final _ChildNotFoundException = Exception('Child with the given ID does not exist');
|
||||||
|
|
||||||
class _CanvasItem {
|
class _ChildInfo {
|
||||||
Offset gsPosition;
|
Offset gsPosition;
|
||||||
final WidgetBuilder builder;
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
_CanvasItem({required this.gsPosition, required this.builder});
|
_ChildInfo({required this.gsPosition, required this.builder});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChildInfo {
|
||||||
|
Offset gsPosition;
|
||||||
|
Offset ssPosition;
|
||||||
|
Widget child;
|
||||||
|
ChildInfo({required this.gsPosition, required this.ssPosition, required this.child});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------ Controller ------------------------------
|
// ------------------------------ Controller ------------------------------
|
||||||
@@ -25,20 +32,30 @@ class _CanvasItem {
|
|||||||
class CanvasController with ChangeNotifier {
|
class CanvasController with ChangeNotifier {
|
||||||
int _nextId = 0; // surely we won't run out of IDs, Clueless
|
int _nextId = 0; // surely we won't run out of IDs, Clueless
|
||||||
Offset _gsTopLeftOffset = Offset.zero;
|
Offset _gsTopLeftOffset = Offset.zero;
|
||||||
final Map<int, _CanvasItem> _children = {}; // int for IDs
|
double _baseScale, _scale;
|
||||||
|
late Size _canvasSize;
|
||||||
|
final Map<int, _ChildInfo> _children = {}; // int for IDs
|
||||||
bool debug;
|
bool debug;
|
||||||
|
|
||||||
CanvasController({this.debug = false});
|
CanvasController({this.debug = false, double initialScale = 1})
|
||||||
|
: _scale = initialScale,
|
||||||
|
_baseScale = initialScale,
|
||||||
|
assert(initialScale > 0, 'Initial scale must be greater than 0');
|
||||||
|
|
||||||
// -------------------- getters --------------------
|
// -------------------- getters --------------------
|
||||||
Offset get offset => _gsTopLeftOffset;
|
Offset get offset => _gsTopLeftOffset;
|
||||||
|
double get scale => _scale;
|
||||||
|
Size get canvasSize => _canvasSize;
|
||||||
// -------------------- public functions --------------------
|
// -------------------- public functions --------------------
|
||||||
|
|
||||||
|
void onCanvasSizeChange(Size size) {
|
||||||
|
_canvasSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
// -------- child manip --------
|
// -------- child manip --------
|
||||||
|
|
||||||
int addChild(Offset position, WidgetBuilder builder) {
|
int addChild(Offset position, WidgetBuilder builder) {
|
||||||
_children[_nextId] = _CanvasItem(gsPosition: position, builder: builder);
|
_children[_nextId] = _ChildInfo(gsPosition: position, builder: builder);
|
||||||
return _nextId++;
|
return _nextId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,26 +80,47 @@ class CanvasController with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onScaleStart(ScaleStartDetails details) {
|
||||||
|
_baseScale = _scale;
|
||||||
|
}
|
||||||
|
|
||||||
// -------- gesture handling --------
|
// -------- gesture handling --------
|
||||||
void handleGesture(ScaleUpdateDetails details) {
|
void onScaleUpdate(ScaleUpdateDetails details) {
|
||||||
// handle gestures like pinch to zoom, pan, etc.
|
// uses usual display conventions and final vector postion - initial vector position
|
||||||
|
|
||||||
// convention is that if I drag from right to left, dx is negative
|
// convention is that if I drag from right to left, dx is negative
|
||||||
// for top to bottom, dy is postive ( so usual display conventions and final vector postion - initial vector position )
|
// for top to bottom, dy is postive
|
||||||
final delta = details.focalPointDelta;
|
|
||||||
|
|
||||||
_gsTopLeftOffset -= delta;
|
// scale + offset is => scale then offset
|
||||||
|
|
||||||
|
if (details.scale != 1) {
|
||||||
|
final newScale = _baseScale * details.scale;
|
||||||
|
_gsTopLeftOffset = newGsTopLeftOnScaling(_gsTopLeftOffset, details.localFocalPoint, _scale, newScale);
|
||||||
|
_scale = newScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.focalPointDelta != Offset.zero) {
|
||||||
|
// if ss distnace is x, and zoom is 2x, gs only moves by x/2
|
||||||
|
_gsTopLeftOffset -= details.focalPointDelta / _scale;
|
||||||
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<(Offset, Widget)> widgetsWithScreenPositions() {
|
void updateScalebyDelta(double delta) {
|
||||||
|
final focalPoint = Offset(canvasSize.width / 2, canvasSize.height / 2);
|
||||||
|
final newScale = _scale + delta;
|
||||||
|
_gsTopLeftOffset = newGsTopLeftOnScaling(_gsTopLeftOffset, focalPoint, _scale, newScale);
|
||||||
|
_scale = newScale;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChildInfo> widgetsWithScreenPositions() {
|
||||||
return _children.entries.map((entry) {
|
return _children.entries.map((entry) {
|
||||||
final item = entry.value;
|
final item = entry.value;
|
||||||
final ssPosition = gsToSs(item.gsPosition, _gsTopLeftOffset);
|
final ssPosition = gsToSs(item.gsPosition, _gsTopLeftOffset, _scale);
|
||||||
var child = item.builder();
|
var child = item.builder();
|
||||||
if (debug) child = _Debug(id: entry.key, gs: item.gsPosition, ss: ssPosition, child: child);
|
if (debug) child = _Debug(id: entry.key, gs: item.gsPosition, ss: ssPosition, child: child);
|
||||||
return (ssPosition, child);
|
return ChildInfo(gsPosition: item.gsPosition, ssPosition: ssPosition, child: child);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-51
@@ -6,11 +6,6 @@ import 'package:infinite_lazy_2d_grid/utils/styles.dart';
|
|||||||
|
|
||||||
import 'background.dart';
|
import 'background.dart';
|
||||||
|
|
||||||
/**
|
|
||||||
* Working of some of the flutter code used here:
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// An infinite canvas that places all the children at the specified positions.
|
/// An infinite canvas that places all the children at the specified positions.
|
||||||
class CanvasView extends StatelessWidget {
|
class CanvasView extends StatelessWidget {
|
||||||
final CanvasBackground canvasBackground;
|
final CanvasBackground canvasBackground;
|
||||||
@@ -23,16 +18,20 @@ class CanvasView extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
color: canvasBackground.bgColor,
|
color: canvasBackground.bgColor,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onScaleUpdate: controller.handleGesture,
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onScaleUpdate: controller.onScaleUpdate,
|
||||||
|
onScaleStart: controller.onScaleStart,
|
||||||
child: ListenableBuilder(
|
child: ListenableBuilder(
|
||||||
listenable: controller,
|
listenable: controller,
|
||||||
builder: (_, _) {
|
builder: (_, _) {
|
||||||
final childrenWithPositions = controller.widgetsWithScreenPositions();
|
final childrenWithPositions = controller.widgetsWithScreenPositions();
|
||||||
final ssPositions = childrenWithPositions.map((e) => e.$1).toList();
|
final ssPositions = childrenWithPositions.map((e) => e.ssPosition).toList();
|
||||||
final children = childrenWithPositions.map((e) => e.$2).toList();
|
final children = childrenWithPositions.map((e) => e.child).toList();
|
||||||
final canvas = _CanvasRenderObject(
|
final canvas = _CanvasRenderObject(
|
||||||
canvasBackground: canvasBackground,
|
canvasBackground: canvasBackground,
|
||||||
ssPositions: ssPositions,
|
ssPositions: ssPositions,
|
||||||
|
scale: controller.scale,
|
||||||
|
onCanvasSizeChange: controller.onCanvasSizeChange,
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -43,7 +42,10 @@ class CanvasView extends StatelessWidget {
|
|||||||
Positioned(
|
Positioned(
|
||||||
top: 16,
|
top: 16,
|
||||||
left: 16,
|
left: 16,
|
||||||
child: Text('Offset: ${controller.offset.coord()}', style: monospaceStyle),
|
child: Text(
|
||||||
|
'Offset: ${controller.offset.coord()}\nScale: ${controller.scale.toStringAsFixed(1)}',
|
||||||
|
style: monospaceStyle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -58,54 +60,79 @@ class CanvasView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A combined widget for all the render object of the children + background.
|
/// A combined widget for all the render object of the children + background.
|
||||||
|
/// Everything is in screen space here
|
||||||
class _CanvasRenderObject extends MultiChildRenderObjectWidget {
|
class _CanvasRenderObject extends MultiChildRenderObjectWidget {
|
||||||
final List<Offset> ssPositions;
|
final List<Offset> ssPositions;
|
||||||
|
final double scale;
|
||||||
final CanvasBackground canvasBackground;
|
final CanvasBackground canvasBackground;
|
||||||
|
final Function onCanvasSizeChange;
|
||||||
|
|
||||||
const _CanvasRenderObject({
|
const _CanvasRenderObject({
|
||||||
required this.ssPositions,
|
required this.ssPositions,
|
||||||
required super.children, // children go to the MultiChildRenderObjectWidget
|
required this.scale,
|
||||||
required this.canvasBackground,
|
required this.canvasBackground,
|
||||||
}) : assert(ssPositions.length == children.length, 'Children and positions must have the same length');
|
required this.onCanvasSizeChange,
|
||||||
|
required super.children, // children go to the MultiChildRenderObjectWidget
|
||||||
|
}) : assert(ssPositions.length == children.length, 'Children and positions must have the same length'),
|
||||||
|
assert(scale != 0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
return _CanvasRenderBox(ssPositions: ssPositions, canvasBackground: canvasBackground);
|
return _CanvasRenderBox(
|
||||||
|
ssPositions: ssPositions,
|
||||||
|
scale: scale,
|
||||||
|
canvasBackground: canvasBackground,
|
||||||
|
onCanvasSizeChange: onCanvasSizeChange,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) {
|
void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) {
|
||||||
renderObject
|
renderObject
|
||||||
..ssPositions = ssPositions
|
..ssPositions = ssPositions
|
||||||
..canvasBackground = canvasBackground;
|
..canvasBackground = canvasBackground
|
||||||
|
..scale = scale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dummy empty parent data for each child
|
class _CanvasWidgetParentData extends ContainerBoxParentData<RenderBox> {
|
||||||
class _WidgetParentData extends ContainerBoxParentData<RenderBox> {}
|
// there is already an "offset" defined in BoxParentdata that is exactly what I want
|
||||||
|
late double scale; // scale is same for all rn but this makes it trivial to expand to children with diff scales
|
||||||
|
}
|
||||||
|
|
||||||
class _CanvasRenderBox extends RenderBox
|
class _CanvasRenderBox extends RenderBox
|
||||||
with
|
with
|
||||||
ContainerRenderObjectMixin<RenderBox, _WidgetParentData>,
|
ContainerRenderObjectMixin<RenderBox, _CanvasWidgetParentData>,
|
||||||
RenderBoxContainerDefaultsMixin<RenderBox, _WidgetParentData> {
|
RenderBoxContainerDefaultsMixin<RenderBox, _CanvasWidgetParentData> {
|
||||||
CanvasBackground _canvasBackground;
|
CanvasBackground _canvasBackground;
|
||||||
List<Offset> _ssPositions;
|
List<Offset> _ssPositions;
|
||||||
|
double _scale;
|
||||||
|
Function onCanvasSizeChange;
|
||||||
|
|
||||||
_CanvasRenderBox({required ssPositions, required canvasBackground})
|
_CanvasRenderBox({required ssPositions, required scale, required canvasBackground, required this.onCanvasSizeChange})
|
||||||
: _ssPositions = ssPositions,
|
: _ssPositions = ssPositions,
|
||||||
|
_scale = scale,
|
||||||
_canvasBackground = canvasBackground;
|
_canvasBackground = canvasBackground;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void setupParentData(RenderBox child) {
|
void setupParentData(RenderBox child) {
|
||||||
if (child.parentData is! _WidgetParentData) {
|
if (child.parentData is! _CanvasWidgetParentData) {
|
||||||
child.parentData = _WidgetParentData();
|
child.parentData = _CanvasWidgetParentData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set ssPositions(List<Offset> ssPositions) {
|
set ssPositions(List<Offset> ssPositions) {
|
||||||
|
assert(ssPositions.length == childCount);
|
||||||
if (_ssPositions != ssPositions) {
|
if (_ssPositions != ssPositions) {
|
||||||
_ssPositions = ssPositions;
|
_ssPositions = ssPositions;
|
||||||
markNeedsPaint();
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set scale(double scale) {
|
||||||
|
if (_scale != scale) {
|
||||||
|
_scale = scale;
|
||||||
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,59 +145,64 @@ class _CanvasRenderBox extends RenderBox
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
|
assert(childCount == _ssPositions.length);
|
||||||
size = constraints.biggest; // expand as much as possible for the parent
|
size = constraints.biggest; // expand as much as possible for the parent
|
||||||
|
onCanvasSizeChange(size); // notify the controller about the size
|
||||||
|
|
||||||
RenderBox? child = firstChild;
|
RenderBox? child = firstChild;
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
while (child != null) {
|
while (child != null) {
|
||||||
final _WidgetParentData parentData = child.parentData! as _WidgetParentData;
|
final _CanvasWidgetParentData childParentData = child.parentData! as _CanvasWidgetParentData;
|
||||||
|
childParentData.offset = _ssPositions[index++];
|
||||||
|
childParentData.scale = _scale;
|
||||||
// loosen so like a stack can take it's own size inside parent
|
// loosen so like a stack can take it's own size inside parent
|
||||||
// like a stack parent is not using size and is always expanded hence the second arg
|
// like a stack parent is not using size and is always expanded hence the second arg
|
||||||
child.layout(constraints.loosen(), parentUsesSize: false);
|
child.layout(constraints.loosen(), parentUsesSize: false);
|
||||||
child = parentData.nextSibling;
|
child = childParentData.nextSibling;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(PaintingContext context, Offset canvasStartOffset) {
|
void paint(PaintingContext context, Offset canvasStartOffset) {
|
||||||
final canvas = context.canvas;
|
assert(childCount == _ssPositions.length);
|
||||||
|
|
||||||
// use the canvas background painter, pass it the canvas and that should handle drawing the background
|
// use the canvas background painter, pass it the canvas and that should handle drawing the background
|
||||||
_canvasBackground.paint(canvas, size);
|
_canvasBackground.paint(context.canvas, size, _scale);
|
||||||
|
|
||||||
|
// though using ssPositionns here directly worked for me but docs using the parentData
|
||||||
|
// to get this info is the convention as child can be reordered ( though this will always
|
||||||
|
// change the offset as well so should work for me ) and this is the flutter way of implementation
|
||||||
|
// on most other stuff ( single source of truth for paint & hit test etc )
|
||||||
RenderBox? child = firstChild;
|
RenderBox? child = firstChild;
|
||||||
for (int idx = 0; child != null; idx++) {
|
while (child != null) {
|
||||||
final _WidgetParentData parentData = child.parentData! as _WidgetParentData;
|
final _CanvasWidgetParentData childParentData = child.parentData! as _CanvasWidgetParentData;
|
||||||
context.paintChild(child, _ssPositions[idx]);
|
|
||||||
child = parentData.nextSibling;
|
context.canvas.save();
|
||||||
|
// relying purely on canvas manips here
|
||||||
|
final drawAt = canvasStartOffset + childParentData.offset;
|
||||||
|
context.canvas.translate(drawAt.dx, drawAt.dy);
|
||||||
|
context.canvas.scale(childParentData.scale, childParentData.scale);
|
||||||
|
context.paintChild(child, Offset.zero); // pain at 0 as already translated
|
||||||
|
|
||||||
|
context.canvas.restore();
|
||||||
|
child = childParentData.nextSibling;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||||
final ssHitPosition = position;
|
|
||||||
|
|
||||||
if (!size.contains(ssHitPosition)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check children in reverse order (last painted = first hit)
|
|
||||||
RenderBox? child = lastChild;
|
RenderBox? child = lastChild;
|
||||||
for (int idx = _ssPositions.length - 1; child != null; idx--) {
|
while (child != null) {
|
||||||
final _WidgetParentData parentData = child.parentData! as _WidgetParentData;
|
// need to get a coordinate in child space
|
||||||
|
final childParentData = child.parentData as _CanvasWidgetParentData;
|
||||||
// Transform the hit position relative to the child's position
|
final Offset childSpacePosition =
|
||||||
final childPosition = ssHitPosition - _ssPositions[idx];
|
(position - childParentData.offset /* <- distance in screen space */ ) / childParentData.scale;
|
||||||
|
// divide by scale so if screen space is 2x, it's only x in child space
|
||||||
if (child.hitTest(BoxHitTestResult.wrap(result), position: childPosition)) {
|
if (child.hitTest(result, position: childSpacePosition)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
child = childParentData.previousSibling;
|
||||||
child = parentData.previousSibling;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
// If no child was hit, the canvas itself handles the hit
|
|
||||||
result.add(BoxHitTestEntry(this, ssHitPosition));
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
/// Convert grid space coordinates to screen space coordinates
|
/// Convert grid space coordinates to screen space coordinates
|
||||||
Offset gsToSs(Offset gsPosition, Offset gsTopLeft) {
|
Offset gsToSs(Offset gsPosition, Offset gsTopLeft, double scale) {
|
||||||
return gsPosition - gsTopLeft;
|
return (gsPosition - gsTopLeft) * scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert screen space coordinates to grid space coordinates
|
/// Convert screen space coordinates to grid space coordinates
|
||||||
Offset ssToGs(Offset ssPosition, Offset gsTopLeft) {
|
Offset ssToGs(Offset ssPosition, Offset gsTopLeft, double scale) {
|
||||||
return ssPosition + gsTopLeft;
|
return ssPosition / scale + gsTopLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset newGsTopLeftOnScaling(Offset gsTopLeft, Offset ssFocalPoint, double oldScale, double newScale) {
|
||||||
|
// gsFocal remains same
|
||||||
|
// we change gsTopLeft to keep ssFocalPoint same as well
|
||||||
|
Offset gsFocalPoint = ssToGs(ssFocalPoint, gsTopLeft, oldScale);
|
||||||
|
return gsFocalPoint - ssFocalPoint / newScale;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user