Skip to content

Commit

Permalink
perf: reduce stroke quality when zoomed out or in previews
Browse files Browse the repository at this point in the history
  • Loading branch information
adil192 committed Oct 26, 2024
1 parent 95c321b commit 40116c5
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 139 deletions.
21 changes: 14 additions & 7 deletions lib/components/canvas/_canvas_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class CanvasPainter extends CustomPainter {
required this.showPageIndicator,
required this.pageIndex,
required this.totalPages,
required this.currentScale,
});

final bool invert;
Expand All @@ -37,11 +38,11 @@ class CanvasPainter extends CustomPainter {
final Stroke? currentStroke;
final SelectResult? currentSelection;
final Color primaryColor;

final EditorPage page;
final bool showPageIndicator;
final int pageIndex;
final int totalPages;
final double currentScale;

@override
void paint(Canvas canvas, Size size) {
Expand Down Expand Up @@ -84,7 +85,7 @@ class CanvasPainter extends CustomPainter {
lastColor = color;
}

canvas.drawPath(stroke.path, Paint()..color = color);
canvas.drawPath(_selectPath(stroke), Paint()..color = color);
}

if (needToRestoreCanvasLayer) canvas.restore();
Expand Down Expand Up @@ -135,11 +136,11 @@ class CanvasPainter extends CustomPainter {
);
} else if (stroke.length <= 2) {
// a dot
final bounds = stroke.path.getBounds();
final bounds = stroke.lowQualityPath.getBounds();
final radius = max(bounds.size.width, stroke.options.size) / 2;
canvas.drawCircle(bounds.center, radius, paint);
} else {
canvas.drawPath(stroke.path, paint);
canvas.drawPath(_selectPath(stroke), paint);
}
}
}
Expand Down Expand Up @@ -168,20 +169,21 @@ class CanvasPainter extends CustomPainter {

if (currentStroke!.length <= 2) {
// a dot
final bounds = currentStroke!.path.getBounds();
final bounds = currentStroke!.highQualityPath.getBounds();
final radius = max(
bounds.size.width * 0.5,
currentStroke!.options.size * 0.25,
);
canvas.drawCircle(bounds.center, radius, paint);
} else {
canvas.drawPath(currentStroke!.path, paint);
// Current stroke always uses high quality
canvas.drawPath(currentStroke!.highQualityPath, paint);
}
}

void _drawLaserStroke(Canvas canvas, LaserStroke stroke) {
canvas.drawPath(
stroke.path,
_selectPath(stroke),
Paint()
..color = stroke.color.withInversion(invert)
..maskFilter = MaskFilter.blur(
Expand Down Expand Up @@ -293,4 +295,9 @@ class CanvasPainter extends CustomPainter {
BlurStyle.normal,
min(size * 0.3, 5),
);

Path _selectPath(Stroke stroke) => switch (currentScale) {
< 1 => stroke.lowQualityPath,
_ => stroke.highQualityPath,
};
}
21 changes: 11 additions & 10 deletions lib/components/canvas/_circle_stroke.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,22 @@ class CircleStroke extends Stroke {
@override
int get length => 25;

/// A list of 25 points that form a circle
/// A list of 24/N points that form a circle
/// with [center] and [radius].
@override
List<Offset> get polygon => super.polygon;

@override
void updatePolygon() {
lastPolygon = List.generate(25, (i) => i / 25 * 2 * pi)
List<Offset> getPolygon(int N) {
final numPoints = 24 ~/ N;
return List.generate(numPoints, (i) => i / numPoints * 2 * pi)
.map((radians) => Offset(cos(radians), sin(radians)))
.map((unitDir) => unitDir * radius + center)
.toList();
lastPath = Path()..addOval(Rect.fromCircle(center: center, radius: radius));
polygonNeedsUpdating = false;
}

/// Returns a [Path] that forms a circle with [center] and [radius].
@override
Path getPath(List<Offset> polygon, {bool smooth = true}) =>
Path()..addOval(Rect.fromCircle(center: center, radius: radius));

@override
@Deprecated('Cannot add points to a circle stroke.')
void addPoint(Offset point, [double? pressure]) {
Expand Down Expand Up @@ -124,7 +125,7 @@ class CircleStroke extends Stroke {
@override
void shift(Offset offset) {
center += offset;
polygonNeedsUpdating = true;
super.shift(offset);
}

@override
Expand All @@ -133,7 +134,7 @@ class CircleStroke extends Stroke {
return RecognizedUnistroke(
DefaultUnistrokeNames.circle,
1,
originalPoints: polygon,
originalPoints: lowQualityPolygon,
referenceUnistrokes: default$1Unistrokes,
);
}
Expand Down
55 changes: 22 additions & 33 deletions lib/components/canvas/_rectangle_stroke.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,39 +77,28 @@ class RectangleStroke extends Stroke {
@override
int get length => 100;

/// A list of points that form the
/// rectangle's perimeter.
/// Each side has 25 points.
/// A list of points that form the rectangle's perimeter.
/// Each side has 24/N points.
@override
List<Offset> get polygon => super.polygon;

List<Offset> getPolygon(int N) => [
// left side
for (int i = 0; i < 24 / N; ++i)
Offset(rect.left, rect.top + rect.height * i / 24),
// bottom side
for (int i = 0; i < 24 / N; ++i)
Offset(rect.left + rect.width * i / 24, rect.bottom),
// right side
for (int i = 0; i < 24 / N; ++i)
Offset(rect.right, rect.bottom - rect.height * i / 24),
// top side
for (int i = 0; i < 24 / N; ++i)
Offset(rect.right - rect.width * i / 24, rect.top),
];

/// Returns a [Path] with four lines for each side of the rectangle.
@override
void updatePolygon() {
lastPolygon = _getPolygon();
lastPath = Path()..addRect(rect);
polygonNeedsUpdating = false;
}

List<Offset> _getPolygon() {
final polygon = <Offset>[];
for (int i = 0; i < 25; ++i) {
// left side
polygon.add(Offset(rect.left, rect.top + rect.height * i / 25));
}
for (int i = 0; i < 25; ++i) {
// bottom side
polygon.add(Offset(rect.left + rect.width * i / 25, rect.bottom));
}
for (int i = 0; i < 25; ++i) {
// right side
polygon.add(Offset(rect.right, rect.bottom - rect.height * i / 25));
}
for (int i = 0; i < 25; ++i) {
// top side
polygon.add(Offset(rect.right - rect.width * i / 25, rect.top));
}
return polygon;
}
Path getPath(List<Offset> polygon, {bool smooth = true}) =>
Path()..addRect(rect);

@override
@Deprecated('Cannot add points to a rectangle stroke.')
Expand Down Expand Up @@ -145,7 +134,7 @@ class RectangleStroke extends Stroke {
@override
void shift(Offset offset) {
rect = rect.shift(offset);
polygonNeedsUpdating = true;
super.shift(offset);
}

@override
Expand All @@ -154,7 +143,7 @@ class RectangleStroke extends Stroke {
return RecognizedUnistroke(
DefaultUnistrokeNames.rectangle,
1,
originalPoints: polygon,
originalPoints: lowQualityPolygon,
referenceUnistrokes: default$1Unistrokes,
);
}
Expand Down
89 changes: 45 additions & 44 deletions lib/components/canvas/_stroke.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:perfect_freehand/perfect_freehand.dart';
import 'package:saber/components/canvas/_circle_stroke.dart';
import 'package:saber/components/canvas/_rectangle_stroke.dart';
import 'package:saber/data/editor/page.dart';
import 'package:saber/data/extensions/list_extensions.dart';
import 'package:saber/data/extensions/point_extensions.dart';
import 'package:saber/data/tools/pen.dart';

Expand All @@ -33,42 +34,33 @@ class Stroke {
bool pressureEnabled;
final StrokeOptions options;

@protected
bool polygonNeedsUpdating = true;
@protected
late List<Offset> lastPolygon = const [];
@protected
late Path lastPath = Path();

List<Offset> get polygon {
if (polygonNeedsUpdating) updatePolygon();
return lastPolygon;
}
List<Offset>? _lowQualityPolygon, _highQualityPolygon;
List<Offset> get lowQualityPolygon => _lowQualityPolygon ??= getPolygon(
// Use every 12th point, or exactly 6 evenly spaced points.
min(12, points.length ~/ 6),
);
List<Offset> get highQualityPolygon => _highQualityPolygon ??= getPolygon(1);

Path get path {
if (polygonNeedsUpdating) updatePolygon();
return lastPath;
}

@protected
void updatePolygon() {
polygonNeedsUpdating = false;
lastPolygon = _getPolygon();
lastPath = _getPath();
}
Path? _lowQualityPath, _highQualityPath;
Path get lowQualityPath =>
_lowQualityPath ??= getPath(lowQualityPolygon, smooth: false);
Path get highQualityPath => _highQualityPath ??= getPath(highQualityPolygon);

void shift(Offset offset) {
if (offset == Offset.zero) return;

for (int i = 0; i < points.length; i++) {
points[i] += offset;
}

polygonNeedsUpdating = true;
points.shift(offset);
_lowQualityPolygon?.shift(offset);
_highQualityPolygon?.shift(offset);
_lowQualityPath?.shift(offset);
_highQualityPath?.shift(offset);
}

void markPolygonNeedsUpdating() {
polygonNeedsUpdating = true;
_lowQualityPolygon = null;
_highQualityPolygon = null;
_lowQualityPath = null;
_highQualityPath = null;
}

Stroke({
Expand Down Expand Up @@ -164,7 +156,7 @@ class Stroke {
}

points.add(PointVector(point.dx, point.dy, pressure));
polygonNeedsUpdating = true;
markPolygonNeedsUpdating();
}

void addPoints(List<Offset> points) {
Expand All @@ -175,7 +167,7 @@ class Stroke {

void popFirstPoint() {
points.removeAt(0);
polygonNeedsUpdating = true;
markPolygonNeedsUpdating();
}

/// Points that are closer than this
Expand Down Expand Up @@ -210,15 +202,16 @@ class Stroke {
}
}

List<Offset> _getPolygon() {
@protected
List<Offset> getPolygon(int N) {
if (!pressureEnabled) {
options.simulatePressure = false;
}
final rememberSimulatedPressure =
options.simulatePressure && options.isComplete;
N <= 1 && options.simulatePressure && options.isComplete;

final polygon = getStroke(
points,
skipPoints(points, N),
options: options,
rememberSimulatedPressure: rememberSimulatedPressure,
);
Expand All @@ -228,26 +221,34 @@ class Stroke {
options.simulatePressure = false;
// Remove points that are too close together
optimisePoints();
// Get polygon again with slightly different input
return _getPolygon();
}

return polygon;
}

/// Returns a [Path] that represents the stroke.
///
/// If the stroke is not complete,
/// the path will just follow the polygon for performance.
/// If [smooth] is true, and the stroke is complete,
/// the path will be a smooth curve between the points in [polygon].
///
/// If the stroke is complete,
/// the path will be a smooth curve between the points.
Path _getPath() {
if (!options.isComplete) {
return Path()..addPolygon(polygon, true);
/// Otherwise, the path will use straight lines between each point
/// in [polygon] for performance.
@protected
Path getPath(List<Offset> polygon, {bool smooth = true}) {
if (smooth && options.isComplete) {
return smoothPathFromPolygon(polygon);
}

return smoothPathFromPolygon(polygon);
return Path()..addPolygon(polygon, true);
}

/// Returns a list with every Nth point in [points].
static List<PointVector> skipPoints(List<PointVector> points, int N) {
if (N == 1) return points;
return [
for (int i = 0; i < points.length; i += N) points[i],
points.last,
];
}

static Path smoothPathFromPolygon(List<Offset> polygon) {
Expand All @@ -270,7 +271,7 @@ class Stroke {

// Remove NaN points, and convert to SVG coordinates
final svgPoints =
polygon.where((offset) => offset.isFinite).map(toSvgPoint);
highQualityPolygon.where((offset) => offset.isFinite).map(toSvgPoint);

return svgPoints.isNotEmpty ? 'M${svgPoints.join('L')}' : '';
}
Expand Down
4 changes: 3 additions & 1 deletion lib/components/canvas/canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Canvas extends StatelessWidget {
required this.currentSelection,
required this.setAsBackground,
required this.currentToolIsSelect,
required this.currentScale,
this.placeholder = false,
});

Expand All @@ -37,7 +38,7 @@ class Canvas extends StatelessWidget {
final void Function(EditorImage image)? setAsBackground;

final bool currentToolIsSelect;

final double currentScale;
final bool placeholder;

@override
Expand Down Expand Up @@ -74,6 +75,7 @@ class Canvas extends StatelessWidget {
currentSelection: currentSelection,
setAsBackground: setAsBackground,
currentToolIsSelect: currentToolIsSelect,
currentScale: currentScale,
),
),
),
Expand Down
Loading

0 comments on commit 40116c5

Please sign in to comment.