Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/flutter_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_notif
export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_result.dart';
export 'package:flutter_map/src/layer/shared/line_patterns/stroke_pattern.dart';
export 'package:flutter_map/src/layer/shared/mobile_layer_transformer.dart';
export 'package:flutter_map/src/layer/shared/shape_effects.dart'
hide shapeEffectsExtent;
export 'package:flutter_map/src/layer/shared/translucent_pointer.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_builder.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
Expand Down
1 change: 1 addition & 0 deletions lib/src/layer/circle_layer/circle_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart';
import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart';
import 'package:flutter_map/src/layer/shared/shape_effects.dart';
import 'package:latlong2/latlong.dart' hide Path;

part 'circle_marker.dart';
Expand Down
14 changes: 14 additions & 0 deletions lib/src/layer/circle_layer/circle_marker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ class CircleMarker<R extends Object> with HitDetectableElement<R> {
/// Whether to treat [radius] as a measurement in meters instead of pixels.
final bool useRadiusInMeter;

/// Optional halo painted around the circle
///
/// Follows the filled disk, or only the border ring when [color] is fully
/// transparent.
final ShapeGlow? glow;

/// Optional shadow painted underneath the circle
///
/// Follows the filled disk, or only the border ring when [color] is fully
/// transparent.
final ShapeShadow? shadow;

@override
final R? hitValue;

Expand All @@ -41,6 +53,8 @@ class CircleMarker<R extends Object> with HitDetectableElement<R> {
this.color = const Color(0xFF00FF00),
this.borderStrokeWidth = 0.0,
this.borderColor = const Color(0xFFFFFF00),
this.glow,
this.shadow,
this.hitValue,
});
}
90 changes: 90 additions & 0 deletions lib/src/layer/circle_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,16 @@ class CirclePainter<R extends Object> extends CustomPainter
final points = <Color, Map<double, List<Offset>>>{};
final pointsFilledBorder = <Color, Map<double, List<Offset>>>{};
final pointsBorder = <Color, Map<double, Map<double, List<Offset>>>>{};
final shadows = <ShapeShadow, List<_CircleOutline>>{};
final glows = <ShapeGlow, List<_CircleOutline>>{};
_pixelsPerMeter = null;
for (final circle in circles) {
final radiusWithoutBorder = _getRadiusInPixel(circle);
final radiusWithBorder =
radiusWithoutBorder + circle.borderStrokeWidth / 2;
final initialCenter = _getOffset(circle.point);
final hasEffects = circle.glow != null || circle.shadow != null;
final effectsExtent = shapeEffectsExtent(circle.glow, circle.shadow);

/// Draws on a "single-world"
WorldWorkControl drawIfVisible(double shift) {
Expand All @@ -80,6 +84,23 @@ class CirclePainter<R extends Object> extends CustomPainter
return false;
}

if (hasEffects && isVisible(radiusWithBorder + effectsExtent)) {
final outline = _CircleOutline.of(
circle,
center: center,
radiusWithoutBorder: radiusWithoutBorder,
radiusWithBorder: radiusWithBorder,
);
if (outline != null) {
if (circle.shadow case final shadow?) {
(shadows[shadow] ??= []).add(outline);
}
if (circle.glow case final glow?) {
(glows[glow] ??= []).add(outline);
}
}
}

if (isVisible(radiusWithoutBorder)) {
points[circle.color] ??= {};
points[circle.color]![radiusWithoutBorder] ??= [];
Expand Down Expand Up @@ -113,6 +134,24 @@ class CirclePainter<R extends Object> extends CustomPainter

// Now that all the points are grouped, let's draw them

// Effects first, so that they stay below every disk and border
for (final MapEntry(key: shadow, value: outlines) in shadows.entries) {
final paint = shadow.toPaint();
canvas
..save()
..translate(shadow.offset.dx, shadow.offset.dy);
for (final outline in outlines) {
outline.paint(canvas, paint);
}
canvas.restore();
}
for (final MapEntry(key: glow, value: outlines) in glows.entries) {
final paint = glow.toPaint();
for (final outline in outlines) {
outline.paint(canvas, paint);
}
}

// First, the border when with non opaque disk
final paintBorder = Paint()..style = PaintingStyle.stroke;
for (final color in pointsBorder.keys) {
Expand Down Expand Up @@ -194,3 +233,54 @@ class CirclePainter<R extends Object> extends CustomPainter
viewportRect
.overlaps(Rect.fromCircle(center: center, radius: radiusInPixel));
}

/// Outermost painted geometry of a [CircleMarker], used to draw its effects
@immutable
class _CircleOutline {
final Offset center;
final double radius;

/// Stroke width when only the border ring is painted, or `null` for the
/// filled disk
final double? ringWidth;

const _CircleOutline._({
required this.center,
required this.radius,
required this.ringWidth,
});

static _CircleOutline? of(
CircleMarker circle, {
required Offset center,
required double radiusWithoutBorder,
required double radiusWithBorder,
}) {
if (circle.color.a > 0) {
return _CircleOutline._(
center: center,
radius: radiusWithBorder,
ringWidth: null,
);
}
if (circle.borderStrokeWidth > 0 && circle.borderColor.a > 0) {
return _CircleOutline._(
center: center,
radius: radiusWithoutBorder,
ringWidth: circle.borderStrokeWidth,
);
}
return null;
}

void paint(Canvas canvas, Paint paint) {
if (ringWidth case final width?) {
paint
..style = PaintingStyle.stroke
..strokeWidth = width;
} else {
paint.style = PaintingStyle.fill;
}
canvas.drawCircle(center, radius, paint);
}
}
54 changes: 50 additions & 4 deletions lib/src/layer/polyline_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,40 @@ class _PolylinePainter<R extends Object> extends CustomPainter

Paint? borderPaint;
Paint? filterPaint;
ShapeGlow? glow;
ShapeShadow? shadow;
int? lastHash;

void drawPaths() {
final hasBorder = borderPaint != null && filterPaint != null;

// Effects follow the outermost outline, and sit below the layer that
// stencils translucent lines
if (glow != null || shadow != null) {
final outlinePath = hasBorder ? borderPath : path;
final outlinePaint = hasBorder ? borderPaint! : paint;

if (shadow case final effect?) {
canvas
..save()
..translate(effect.offset.dx, effect.offset.dy)
..drawPath(
outlinePath,
_outlineEffectPaint(effect.toPaint(), outlinePaint),
)
..restore();
}
if (glow case final effect?) {
canvas.drawPath(
outlinePath,
_outlineEffectPaint(effect.toPaint(), outlinePaint),
);
}

glow = null;
shadow = null;
}

if (hasBorder) {
if (needsLayerSaving) {
canvas.saveLayer(viewportRect, Paint());
Expand Down Expand Up @@ -148,7 +178,10 @@ class _PolylinePainter<R extends Object> extends CustomPainter
strokeWidth = polyline.strokeWidth;
}

if (!areOffsetsVisible(offsets, strokeWidth)) {
final effectsExtent =
shapeEffectsExtent(polyline.glow, polyline.shadow);

if (!areOffsetsVisible(offsets, strokeWidth + 2 * effectsExtent)) {
return WorldWorkControl.invisible;
}

Expand All @@ -160,6 +193,9 @@ class _PolylinePainter<R extends Object> extends CustomPainter
needsLayerSaving = polyline.color.a < 1 ||
(polyline.gradientColors?.any((c) => c.a < 1) ?? false);

glow = polyline.glow;
shadow = polyline.shadow;

// strokeWidth, or strokeWidth + borderWidth if relevant.
double largestStrokeWidth = strokeWidth;

Expand Down Expand Up @@ -204,6 +240,9 @@ class _PolylinePainter<R extends Object> extends CustomPainter
..blendMode = BlendMode.dstOut;
}

// Keep segments whose effects reach into the viewport
final hikerStrokeWidth = largestStrokeWidth + 2 * effectsExtent;

final radius = paint.strokeWidth / 2;
final borderRadius = (borderPaint?.strokeWidth ?? 0) / 2;

Expand All @@ -218,7 +257,7 @@ class _PolylinePainter<R extends Object> extends CustomPainter
offsets: offsets,
closePath: false,
canvasSize: size,
strokeWidth: largestStrokeWidth,
strokeWidth: hikerStrokeWidth,
);
hiker.addAllVisibleSegments(paths);
} else if (isDotted) {
Expand All @@ -228,7 +267,7 @@ class _PolylinePainter<R extends Object> extends CustomPainter
patternFit: polyline.pattern.patternFit!,
closePath: false,
canvasSize: size,
strokeWidth: largestStrokeWidth,
strokeWidth: hikerStrokeWidth,
);

final List<double> radii = [];
Expand All @@ -251,7 +290,7 @@ class _PolylinePainter<R extends Object> extends CustomPainter
patternFit: polyline.pattern.patternFit!,
closePath: false,
canvasSize: size,
strokeWidth: largestStrokeWidth,
strokeWidth: hikerStrokeWidth,
);

for (final visibleSegment in hiker.getAllVisibleSegments()) {
Expand All @@ -271,6 +310,13 @@ class _PolylinePainter<R extends Object> extends CustomPainter
drawPaths();
}

Paint _outlineEffectPaint(Paint effectPaint, Paint outlinePaint) =>
effectPaint
..strokeWidth = outlinePaint.strokeWidth
..strokeCap = outlinePaint.strokeCap
..strokeJoin = outlinePaint.strokeJoin
..style = outlinePaint.style;

ui.Gradient _paintGradient(Polyline polyline, List<Offset> offsets) =>
ui.Gradient.linear(offsets.first, offsets.last, polyline.gradientColors!,
_getColorsStop(polyline));
Expand Down
12 changes: 12 additions & 0 deletions lib/src/layer/polyline_layer/polyline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ class Polyline<R extends Object> with HitDetectableElement<R> {
/// Set to true if the width of the stroke should have meters as unit.
final bool useStrokeWidthInMeter;

/// Optional halo painted around the line (and its border)
final ShapeGlow? glow;

/// Optional shadow painted underneath the line (and its border)
final ShapeShadow? shadow;

@override
final R? hitValue;

Expand All @@ -61,6 +67,8 @@ class Polyline<R extends Object> with HitDetectableElement<R> {
this.strokeCap = StrokeCap.round,
this.strokeJoin = StrokeJoin.round,
this.useStrokeWidthInMeter = false,
this.glow,
this.shadow,
this.hitValue,
});

Expand All @@ -76,6 +84,8 @@ class Polyline<R extends Object> with HitDetectableElement<R> {
strokeCap == other.strokeCap &&
strokeJoin == other.strokeJoin &&
useStrokeWidthInMeter == other.useStrokeWidthInMeter &&
glow == other.glow &&
shadow == other.shadow &&
hitValue == other.hitValue &&
// Expensive computations last to take advantage of lazy logic gates
listEquals(colorsStop, other.colorsStop) &&
Expand All @@ -97,6 +107,8 @@ class Polyline<R extends Object> with HitDetectableElement<R> {
strokeCap,
strokeJoin,
useStrokeWidthInMeter,
glow,
shadow,
);

int? _hashCode;
Expand Down
1 change: 1 addition & 0 deletions lib/src/layer/polyline_layer/polyline_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_de
import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart';
import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart';
import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart';
import 'package:flutter_map/src/layer/shared/shape_effects.dart';
import 'package:flutter_map/src/misc/extensions.dart';
import 'package:flutter_map/src/misc/offsets.dart';
import 'package:flutter_map/src/misc/simplify.dart';
Expand Down
Loading