diff --git a/config.lock.yaml b/config.lock.yaml index 9274ef1..fb7190c 100644 --- a/config.lock.yaml +++ b/config.lock.yaml @@ -1,6 +1,7 @@ adaptive_number: ea9178fdd4d82ac45cf0ec966ac870dae661124f dots_indicator: 508f5883ac79bdbc10254092de3f28f571d261cd ed25519_edwards: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf +hand_signature: 1beedb164d093643365b0832277c377353c7464f hashlib: 983cdbd5ee2529b908876b57a7217c09c6bc148a hashlib_codecs: 2a966c37c3b9b1f5541ae88e99ab34acf3fc968b introduction_screen: 4a90e557630b28834479ed9c64a9d2d0185d8e48 diff --git a/config.yaml b/config.yaml index 6b2df32..aafb57a 100644 --- a/config.yaml +++ b/config.yaml @@ -44,3 +44,6 @@ libsignal_protocol_dart: git: https://github.com/bcgit/pc-dart.git x25519: git: https://github.com/Tougee/curve25519.git + +hand_signature: + git: https://github.com/RomanBase/hand_signature.git \ No newline at end of file diff --git a/dots_indicator/pubspec.lock b/dots_indicator/pubspec.lock new file mode 100644 index 0000000..19fc718 --- /dev/null +++ b/dots_indicator/pubspec.lock @@ -0,0 +1,71 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" +sdks: + dart: ">=3.8.0-0 <4.0.0" diff --git a/hand_signature/LICENSE b/hand_signature/LICENSE new file mode 100644 index 0000000..4360507 --- /dev/null +++ b/hand_signature/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 RomanBase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hand_signature/lib/signature.dart b/hand_signature/lib/signature.dart new file mode 100644 index 0000000..217b0bc --- /dev/null +++ b/hand_signature/lib/signature.dart @@ -0,0 +1,5 @@ +export 'src/signature_control.dart'; +export 'src/signature_drawer.dart'; +export 'src/signature_paint.dart'; +export 'src/signature_painter.dart'; +export 'src/signature_view.dart'; diff --git a/hand_signature/lib/src/signature_control.dart b/hand_signature/lib/src/signature_control.dart new file mode 100644 index 0000000..332d01e --- /dev/null +++ b/hand_signature/lib/src/signature_control.dart @@ -0,0 +1,1399 @@ +import 'dart:math' as math; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import '../signature.dart'; +import 'utils.dart'; + +/// @Deprecated('Paint parameters are obsolete from 3.1.0 and will be removed in future versions. Use SignaturePathSetup instead.') +/// Paint settings. +/// This class is used for backwards compatibility. +class SignaturePaintParams { + /// Color of line. + final Color color; + + /// Minimal width of line. + final double strokeWidth; + + /// Maximal width of line. + final double maxStrokeWidth; + + /// Hex value of [color]. + String get hexColor => color.hexValue; + + /// Opacity of [color]. + String get opacity => '${color.a}}'; + + /// Paint settings of line. + /// [color] - color of line. + /// [strokeWidth] - minimal width of line. + /// [maxStrokeWidth] - maximal width of line. + const SignaturePaintParams({ + this.color = Colors.black, + this.strokeWidth = 1.0, + this.maxStrokeWidth = 10.0, + }); +} + +/// Defines the setup parameters for a signature path, including smoothing, velocity, and pressure ratios. +class SignaturePathSetup { + /// Minimal distance between two control points. + final double threshold; + + /// Ratio of line smoothing. + /// Don't have impact to performance. Values between 0 - 1. + /// [0] - no smoothing, no flattening. + /// [1] - best smoothing, but flattened. + /// Best results are between: 0.5 - 0.85. + final double smoothRatio; + + /// Clamps velocity. + final double velocityRange; + + /// Ratio between pressure and velocity. + /// 1.0 - only pressure, velocity is ignored + /// 0.0 - only velocity, pressure is ignored + /// 0.5 - balanced pressure and velocity + final double pressureRatio; + + /// Additional arguments to setup path - typically used with custom {HandSignatureDrawer} + /// Only primitives should be stored in args [String, num, List, Map] - just structs that are supported with jsonEncode/jsonDecode converter. + final Map? args; + + const SignaturePathSetup({ + this.threshold = 3.0, + this.smoothRatio = 0.65, + this.velocityRange = 2.0, + this.pressureRatio = 0.0, + this.args, + }) : assert(threshold > 0.0), + assert(smoothRatio > 0.0 && smoothRatio <= 1.0), + assert(velocityRange > 0.0), + assert(pressureRatio >= 0.0 && pressureRatio <= 1.0); + + factory SignaturePathSetup.fromMap(Map data) => + SignaturePathSetup( + threshold: data['threshold'], + smoothRatio: data['smoothRatio'], + velocityRange: data['velocityRange'], + pressureRatio: data['pressureRatio'], + args: data['args'], + ); + + Map toMap() => { + 'threshold': threshold, + 'smoothRatio': smoothRatio, + 'velocityRange': velocityRange, + 'pressureRatio': pressureRatio, + if (args != null) 'args': args, + }; +} + +/// Extended [Offset] point with [timestamp] and optional [pressure]. +class OffsetPoint extends Offset { + /// Timestamp of this point. Used to determine velocity to other points. + final int timestamp; + + /// The pressure value at this point, if available. + final double? pressure; + + /// Creates an [OffsetPoint] with the given coordinates, timestamp, and optional pressure. + const OffsetPoint({ + required double dx, + required double dy, + required this.timestamp, + this.pressure, + }) : super(dx, dy); + + /// Creates an [OffsetPoint] from a standard [Offset] with the current timestamp and optional pressure. + factory OffsetPoint.from(Offset offset, {double? pressure}) => OffsetPoint( + dx: offset.dx, + dy: offset.dy, + timestamp: DateTime.now().millisecondsSinceEpoch, + pressure: pressure, + ); + + /// Creates an [OffsetPoint] from a map of data, typically used for deserialization. + factory OffsetPoint.fromMap(Map data) => OffsetPoint( + dx: data['x'], + dy: data['y'], + timestamp: data['t'], + pressure: data['p'], + ); + + /// Converts this [OffsetPoint] to a map of data, typically used for serialization. + Map toMap() => { + 'x': dx, + 'y': dy, + 't': timestamp, + if (pressure != null) 'p': pressure, + }; + + /// Calculates the velocity between this point and a [other] (previous) point. + /// Returns 0.0 if timestamps are the same to avoid division by zero. + double velocityFrom(OffsetPoint other) => timestamp != other.timestamp + ? distanceTo(other) / (timestamp - other.timestamp) + : 0.0; + + @override + OffsetPoint translate(double translateX, double translateY) { + return OffsetPoint( + dx: dx + translateX, + dy: dy + translateY, + timestamp: timestamp, + pressure: pressure, + ); + } + + @override + OffsetPoint scale(double scaleX, double scaleY) { + return OffsetPoint( + dx: dx * scaleX, + dy: dy * scaleY, + timestamp: timestamp, + pressure: pressure, + ); + } + + @override + bool operator ==(Object other) { + return other is OffsetPoint && + other.dx == dx && + other.dy == dy && + other.timestamp == timestamp && + other.pressure == pressure; + } + + @override + int get hashCode => Object.hash(super.hashCode, timestamp, pressure); +} + +/// Represents a cubic Bezier curve segment, defined by a start point, end point, and two control points. +/// This class extends [Offset] to represent the starting point of the curve. +/// +/// For more information on Bezier curves, refer to: https://cubic-bezier.com/ +class CubicLine extends Offset { + /// The starting point of the cubic Bezier curve. + final OffsetPoint start; + + /// The first control point, influencing the curve's shape from the [start] point. + final Offset cpStart; + + /// The second control point, influencing the curve's shape from the [end] point. + final Offset cpEnd; + + /// The ending point of the cubic Bezier curve. + final OffsetPoint end; + + /// The calculated velocity of the line segment. + late double _velocity; + + /// The calculated distance between the start and end points. + late double _distance; + + /// Cached 'Up' vector for the [start] point, used for shape calculations. + Offset? _upStartVector; + + /// The 'Up' vector for the [start] point, calculated if not already cached. + /// This vector is perpendicular to the curve's direction at the start. + Offset get upStartVector => + _upStartVector ?? + (_upStartVector = start.directionTo(point(0.001)).rotate(-math.pi * 0.5)); + + /// Cached 'Up' vector for the [end] point, used for shape calculations. + Offset? _upEndVector; + + /// The 'Up' vector for the [end] point, calculated if not already cached. + /// This vector is perpendicular to the curve's direction at the end. + Offset get upEndVector => + _upEndVector ?? + (_upEndVector = end.directionTo(point(0.999)).rotate(math.pi * 0.5)); + + /// The 'Down' vector for the [start] point, which is the [upStartVector] rotated by 180 degrees. + Offset get _downStartVector => upStartVector.rotate(math.pi); + + /// The 'Down' vector for the [end] point, which is the [upEndVector] rotated by 180 degrees. + Offset get _downEndVector => upEndVector.rotate(math.pi); + + /// The size ratio of the line at its starting point (typically 0.0 to 1.0). + double startSize; + + /// The size ratio of the line at its ending point (typically 0.0 to 1.0). + double endSize; + + /// Indicates if this line segment represents a 'dot' (i.e., start and end points are the same, and velocity is zero). + bool get isDot => _velocity == 0.0; + + /// Creates a [CubicLine] segment. + /// + /// [start] The initial point of the curve. + /// [end] The end point of the curve. + /// [cpStart] The control point associated with the [start] vector. + /// [cpEnd] The control point associated with the [end] vector. + /// [upStartVector] An optional pre-calculated 'Up' vector for the start point. + /// [upEndVector] An optional pre-calculated 'Up' vector for the end point. + /// [startSize] The initial size ratio of the line at the beginning of the curve. + /// [endSize] The final size ratio of the line at the end of the curve. + CubicLine({ + required this.start, + required this.cpStart, + required this.cpEnd, + required this.end, + Offset? upStartVector, + Offset? upEndVector, + this.startSize = 0.0, + this.endSize = 0.0, + }) : super(start.dx, start.dy) { + _upStartVector = upStartVector; + _upEndVector = upEndVector; + _velocity = end.velocityFrom(start); + _distance = start.distanceTo(end); + } + + @override + CubicLine scale(double scaleX, double scaleY) => CubicLine( + start: start.scale(scaleX, scaleY), + cpStart: cpStart.scale(scaleX, scaleY), + cpEnd: cpEnd.scale(scaleX, scaleY), + end: end.scale(scaleX, scaleY), + upStartVector: _upStartVector, + upEndVector: _upEndVector, + startSize: startSize * (scaleX + scaleY) * 0.5, + endSize: endSize * (scaleX + scaleY) * 0.5, + ); + + @override + CubicLine translate(double translateX, double translateY) => CubicLine( + start: start.translate(translateX, translateY), + cpStart: cpStart.translate(translateX, translateY), + cpEnd: cpEnd.translate(translateX, translateY), + end: end.translate(translateX, translateY), + upStartVector: _upStartVector, + upEndVector: _upEndVector, + startSize: startSize, + endSize: endSize, + ); + + /// Calculates the approximate length of the cubic curve with a given [accuracy]. + /// + /// [accuracy] A value between 0 (fastest, raw accuracy) and 1 (slowest, most accurate). + /// Returns the calculated length of the curve. + double length({double accuracy = 0.1}) { + final steps = (accuracy * 100).toInt(); + + if (steps <= 1) { + return _distance; + } + + double length = 0.0; + + Offset prevPoint = start; + for (int i = 1; i < steps; i++) { + final t = i / steps; + + final next = point(t); + + length += prevPoint.distanceTo(next); + prevPoint = next; + } + + return length; + } + + /// Calculates a point on the cubic curve at a given parameter [t]. + /// + /// [t] A value between 0 (start of the curve) and 1 (end of the curve). + /// Returns the [Offset] representing the location on the curve at [t]. + Offset point(double t) { + final rt = 1.0 - t; + return (start * rt * rt * rt) + + (cpStart * 3.0 * rt * rt * t) + + (cpEnd * 3.0 * rt * t * t) + + (end * t * t * t); + } + + /// Calculates the velocity along this line segment. + /// + /// [accuracy] The accuracy for calculating the length of the curve. + /// Returns the velocity, or 0.0 if start and end timestamps are the same. + double velocity({double accuracy = 0.0}) => start.timestamp != end.timestamp + ? length(accuracy: accuracy) / (end.timestamp - start.timestamp) + : 0.0; + + /// Combines the line's intrinsic velocity with an [inVelocity] based on a [velocityRatio]. + /// + /// [inVelocity] The incoming velocity to combine with. + /// [velocityRatio] The ratio to weigh the line's intrinsic velocity (0.0 to 1.0). + /// [maxFallOff] The maximum allowed difference between the combined velocity and [inVelocity]. + /// Returns the combined velocity. + double combineVelocity(double inVelocity, + {double velocityRatio = 0.65, double maxFallOff = 1.0}) { + final value = + (_velocity * velocityRatio) + (inVelocity * (1.0 - velocityRatio)); + + maxFallOff *= _distance / 10.0; + + final dif = value - inVelocity; + if (dif.abs() > maxFallOff) { + if (dif > 0.0) { + return inVelocity + maxFallOff; + } else { + return inVelocity - maxFallOff; + } + } + + return value; + } + + /// Converts this cubic line segment into a Flutter [Path] object. + Path toPath() => Path() + ..moveTo(dx, dy) + ..cubicTo(cpStart.dx, cpStart.dy, cpEnd.dx, cpEnd.dy, end.dx, end.dy); + + /// Converts this cubic line into a list of [CubicArc] segments. + /// This is used to approximate the curve with a series of arcs for drawing. + /// + /// [size] The base size for the arcs. + /// [deltaSize] The change in size across the arc. + /// [precision] The precision for generating arc segments (higher value means more segments). + /// Returns a list of [CubicArc] objects. + List toArc({double? deltaSize, double precision = 0.5}) { + final list = []; + + final steps = (_distance * precision).floor().clamp(1, 30); + + Offset start = this.start; + for (int i = 0; i < steps; i++) { + final t = (i + 1) / steps; + final loc = point(t); + final width = startSize + (deltaSize ?? (endSize - startSize)) * t; + + list.add(CubicArc( + start: start, + location: loc, + size: width, + )); + + start = loc; + } + + return list; + } + + /// Converts this cubic line into a closed [Path] representing a filled shape. + /// This is typically used for drawing thick, filled lines. + /// + /// [size] The base stroke width. + /// [maxSize] The maximum stroke width. + Path toShape(double size, double maxSize) { + final startArm = (size + (maxSize - size) * startSize) * 0.5; + final endArm = (size + (maxSize - size) * endSize) * 0.5; + + final sDirUp = upStartVector; + final eDirUp = upEndVector; + + final d1 = sDirUp * startArm; + final d2 = eDirUp * endArm; + final d3 = eDirUp.rotate(math.pi) * endArm; + final d4 = sDirUp.rotate(math.pi) * startArm; + + return Path() + ..start(start + d1) + ..cubic(cpStart + d1, cpEnd + d2, end + d2) + ..line(end + d3) + ..cubic(cpEnd + d3, cpStart + d4, start + d4) + ..close(); + } + + /// Returns the 'Up' offset for the start point, scaled by the start radius. + Offset cpsUp(double size, double maxSize) => + upStartVector * startRadius(size, maxSize); + + /// Returns the 'Up' offset for the end point, scaled by the end radius. + Offset cpeUp(double size, double maxSize) => + upEndVector * endRadius(size, maxSize); + + /// Returns the 'Down' offset for the start point, scaled by the start radius. + Offset cpsDown(double size, double maxSize) => + _downStartVector * startRadius(size, maxSize); + + /// Returns the 'Down' offset for the end point, scaled by the end radius. + Offset cpeDown(double size, double maxSize) => + _downEndVector * endRadius(size, maxSize); + + /// Returns the calculated radius for the start point based on [size], [maxSize], and [startSize]. + double startRadius(double size, double maxSize) => + _lerpRadius(size, maxSize, startSize); + + /// Returns the calculated radius for the end point based on [size], [maxSize], and [endSize]. + double endRadius(double size, double maxSize) => + _lerpRadius(size, maxSize, endSize); + + /// Performs linear interpolation to calculate a radius based on a given size, max size, and interpolation factor. + /// + /// [size] The base size. + /// [maxSize] The maximum size. + /// [t] The interpolation factor (0.0 to 1.0). + /// Returns the interpolated radius. + double _lerpRadius(double size, double maxSize, double t) => + (size + (maxSize - size) * t) * 0.5; + + /// Calculates a 'soft' control point for a [current] point based on its [previous] and [next] neighbors. + /// This is used to create smooth curves. + /// + /// [current] The current [OffsetPoint] for which to calculate the control point. + /// [previous] The preceding [OffsetPoint] in the path. + /// [next] The succeeding [OffsetPoint] in the path. + /// [reverse] If true, calculates the control point in reverse direction. + /// [smoothing] A factor (0.0 to 1.0) controlling the smoothness of the curve. + /// Returns the calculated soft control point as an [Offset]. + static Offset softCP(OffsetPoint current, + {OffsetPoint? previous, + OffsetPoint? next, + bool reverse = false, + double smoothing = 0.65}) { + assert(smoothing >= 0.0 && smoothing <= 1.0); + + previous ??= current; + next ??= current; + + final sharpness = 1.0 - smoothing; + + final dist1 = previous.distanceTo(current); + final dist2 = current.distanceTo(next); + final dist = dist1 + dist2; + final dir1 = current.directionTo(next); + final dir2 = current.directionTo(previous); + final dir3 = + reverse ? next.directionTo(previous) : previous.directionTo(next); + + final velocity = + (dist * 0.3 / (next.timestamp - previous.timestamp)).clamp(0.5, 3.0); + final ratio = (dist * velocity * smoothing) + .clamp(0.0, (reverse ? dist2 : dist1) * 0.5); + + final dir = + ((reverse ? dir2 : dir1) * sharpness) + (dir3 * smoothing) * ratio; + final x = current.dx + dir.dx; + final y = current.dy + dir.dy; + + return Offset(x, y); + } + + @override + bool operator ==(Object other) => + other is CubicLine && + start == other.start && + cpStart == other.cpStart && + cpEnd == other.cpEnd && + end == other.end && + startSize == other.startSize && + endSize == other.endSize; + + @override + int get hashCode => + super.hashCode ^ + start.hashCode ^ + cpStart.hashCode ^ + cpEnd.hashCode ^ + end.hashCode ^ + startSize.hashCode ^ + endSize.hashCode; +} + +/// Represents an arc segment between two points, typically used for drawing. +/// This class extends [Offset] to represent the starting point of the arc. +class CubicArc extends Offset { + /// The ending location of the arc. + final Offset location; + + /// The size of the line segment represented by this arc (typically 0.0 to 1.0). + final double size; + + /// Generates a [Path] object representing this arc. + Path get path => Path() + ..moveTo(dx, dy) + ..arcToPoint(location, rotation: pi2); + + /// Returns a [Rect] that encloses both the start and end points of the arc. + Rect get rect => Rect.fromPoints(this, location); + + /// Creates a [CubicArc] instance. + /// + /// [start] The starting point of the arc. + /// [location] The ending point of the arc. + /// [size] The size ratio of the arc, typically between 0 and 1. + CubicArc({ + required Offset start, + required this.location, + this.size = 1.0, + }) : super(start.dx, start.dy); + + @override + Offset translate(double translateX, double translateY) => CubicArc( + start: Offset(dx + translateX, dy + translateY), + location: location.translate(translateX, translateY), + size: size, + ); + + @override + Offset scale(double scaleX, double scaleY) => CubicArc( + start: Offset(dx * scaleX, dy * scaleY), + location: location.scale(scaleX, scaleY), + size: size * (scaleX + scaleY) * 0.5, + ); +} + +/// Manages a sequence of points to form a smooth, drawable path using cubic Bezier curves. +class CubicPath { + /// The raw list of [OffsetPoint]s that define the path. + final _points = []; + + /// The list of [CubicLine] segments derived from the raw points, forming the smoothed path. + final _lines = []; + + /// The setup parameters for this path, including smoothing, velocity, and pressure ratios. + final SignaturePathSetup setup; + + /// Returns an unmodifiable list of the raw [OffsetPoint]s that make up this path. + List get points => _points; + + /// Returns an unmodifiable list of the [CubicLine] segments that form this path. + List get lines => _lines; + + /// The first point in the path, or `null` if the path is empty. + Offset? get _origin => _points.isNotEmpty ? _points[0] : null; + + /// The last point added to the path, or `null` if the path is empty. + OffsetPoint? get _lastPoint => + _points.isNotEmpty ? _points[_points.length - 1] : null; + + /// Indicates whether the path contains any drawn lines. + bool get isFilled => _lines.isNotEmpty; + + /// Indicates whether the path consists of a single 'dot' (a line with zero velocity). + bool get isDot => lines.length == 1 && lines[0].isDot; + + /// The maximum velocity observed within this path. + double _maxVelocity = 1.0; + + /// The current average velocity of the path being drawn. + double _currentVelocity = 0.0; + + /// The current size (thickness) of the line based on velocity and pressure. + double _currentSize = 0.0; + + /// Creates a [CubicPath] with the given [setup] parameters. + CubicPath({ + this.setup = const SignaturePathSetup(), + }) { + _maxVelocity = setup.velocityRange; + } + + List toArcs() { + final arcs = []; + + for (final line in _lines) { + arcs.addAll(line.toArc()); + } + + return arcs; + } + + /// Adds a [CubicLine] segment to the path. + /// This method updates the current velocity and size based on the new line. + void _addLine(CubicLine line) { + if (_lines.isEmpty) { + if (_currentVelocity == 0.0) { + _currentVelocity = line._velocity; + } + + if (_currentSize == 0.0) { + _currentSize = + _lineSize(_currentVelocity, _maxVelocity, line.start.pressure); + } + } else { + line._upStartVector = _lines.last.upEndVector; + } + + _lines.add(line); + + final combinedVelocity = + line.combineVelocity(_currentVelocity, maxFallOff: 0.125); + final double endSize = + _lineSize(combinedVelocity, _maxVelocity, line.end.pressure); + + if (combinedVelocity > _maxVelocity) { + _maxVelocity = combinedVelocity; + } + + line.startSize = _currentSize; + line.endSize = endSize; + + //_arcs.addAll(line.toArc()); + + _currentSize = endSize; + _currentVelocity = combinedVelocity; + } + + /// Adds a 'dot' (a single point line) to the path. + /// This is used when the path consists of a single, stationary point. + void _addDot(CubicLine line) { + final size = 0.25 + + _lineSize(_currentVelocity, _maxVelocity, line.end.pressure) * 0.5; + line.startSize = size; + line.endSize = size; + + _lines.add(line); + //_arcs.addAll(line.toArc(deltaSize: 0.0)); + } + + /// Calculates the line size (thickness) based on the given [velocity], maximum velocity [max], and optional [pressure]. + double _lineSize(double velocity, double max, double? pressure) { + velocity /= max; + + if (pressure != null) { + final v = (1.0 - velocity) * (1.0 - setup.pressureRatio); + final p = pressure * setup.pressureRatio; + + return (v + p).clamp(0.0, 1.0); + } + + return 1.0 - velocity.clamp(0.0, 1.0); + } + + /// Starts a new path at the given [point]. + /// This method must be called before [add] or [end]. + /// + /// [point] The initial [Offset] for the path. + /// [velocity] The initial velocity of the path. + /// [pressure] The initial pressure at the starting point. + void begin(Offset point, {double velocity = 0.0, double? pressure}) { + _points.add(point is OffsetPoint + ? point + : OffsetPoint.from(point, pressure: pressure)); + _currentVelocity = velocity; + } + + /// Adds a new [point] to the active path. + /// This method calculates new cubic line segments and updates the path. + /// + /// [point] The new [Offset] to add to the path. + /// [pressure] The pressure at the new point. + void add(Offset point, {double? pressure}) { + assert(_origin != null); + + final nextPoint = point is OffsetPoint + ? point + : OffsetPoint.from(point, pressure: pressure); + + if (_lastPoint == null || + _lastPoint!.distanceTo(nextPoint) < setup.threshold) { + return; + } + + _points.add(nextPoint); + int count = _points.length; + + if (count < 3) { + return; + } + + int i = count - 3; + + final prev = i > 0 ? _points[i - 1] : _points[i]; + final start = _points[i]; + final end = _points[i + 1]; + final next = _points[i + 2]; + + final cpStart = CubicLine.softCP( + start, + previous: prev, + next: end, + smoothing: setup.smoothRatio, + ); + + final cpEnd = CubicLine.softCP( + end, + previous: start, + next: next, + smoothing: setup.smoothRatio, + reverse: true, + ); + + final line = CubicLine( + start: start, + cpStart: cpStart, + cpEnd: cpEnd, + end: end, + ); + + _addLine(line); + } + + /// Ends the active path at the given [point]. + /// This method finalizes the path segments and handles cases for very short paths (dots or single lines). + /// + /// [point] The final [Offset] for the path. + /// [pressure] The pressure at the final point. + /// Returns `true` if the path was successfully ended and is valid, `false` otherwise. + bool end({Offset? point, double? pressure}) { + if (point != null) { + add(point, pressure: pressure); + } + + if (_points.isEmpty) { + return false; + } + + if (_points.length < 3) { + if (_points.length == 1 || _points[0].distanceTo(points[1]) == 0.0) { + _addDot(CubicLine( + start: _points[0], + cpStart: _points[0], + cpEnd: _points[0], + end: _points[0], + )); + } else { + _addLine(CubicLine( + start: _points[0], + cpStart: _points[0], + cpEnd: _points[1], + end: _points[1], + )); + } + } else { + final i = _points.length - 3; + + if (_points[i + 1].distanceTo(points[i + 2]) > 0.0) { + _addLine(CubicLine( + start: _points[i + 1], + cpStart: _points[i + 1], + cpEnd: _points[i + 2], + end: _points[i + 2], + )); + } + } + + return true; + } + + /// Scales the entire path by a given [ratio]. + /// This method updates all points, arcs, and lines within the path. + void setScale(double ratio) { + if (!isFilled) { + return; + } + + final pointsData = PathUtil.scale(_points, ratio); + _points + ..clear() + ..addAll(pointsData); + + final lineData = PathUtil.scale(_lines, ratio); + _lines + ..clear() + ..addAll(lineData); + } + + CubicPath copy() => CubicPath(setup: setup) + .._points.addAll(_points) + .._lines.addAll(_lines); + + /// Clears all data associated with this path, effectively resetting it. + void clear() { + _points.clear(); + _lines.clear(); + } + + /// Checks if this [CubicPath] is equal to [other] based on their raw points. + bool equals(CubicPath other) { + if (points.length == other.points.length) { + for (int i = 0; i < points.length; i++) { + if (points[i] != other.points[i]) { + return false; + } + } + + return true; + } + + return false; + } +} + +/// A [ChangeNotifier] that controls the drawing and manipulation of a hand signature. +/// It manages the active paths, their setup, and provides methods for +/// starting, altering, closing, importing, and exporting signature data. +class HandSignatureControl extends ChangeNotifier { + /// A private list storing all completed [CubicPath]s that form the signature. + final _paths = []; + + /// A function that provides the [SignaturePathSetup] for new paths. + late SignaturePathSetup Function() setup; + + /// Optional visual parameters for line painting, primarily for backwards compatibility. + SignaturePaintParams? params; + + /// The currently active (unfinished) [CubicPath] being drawn. + CubicPath? _activePath; + + /// The size of the canvas area where the signature is drawn. + /// TODO: This property should ideally be part of [SignaturePaintParams] or a dedicated rendering context. + Size _areaSize = Size.zero; + + /// Returns an unmodifiable list of all completed [CubicPath]s. + List get paths => _paths; + + /// Returns a lazy list of all raw [Offset] control points from all paths. + List> get _offsets => _paths.map((data) => data.points).toList(); + + /// Returns a lazy list of all [CubicLine] segments from all paths. + List> get _cubicLines => + _paths.map((data) => data.lines).toList(); + + /// Returns a flattened list of all [CubicLine] segments across all paths. + List get lines => _paths.expand((data) => data.lines).toList(); + + /// Indicates whether there is an active (unfinished) path being drawn. + bool get hasActivePath => _activePath != null; + + /// Indicates whether any signature data has been drawn (i.e., if there are any completed paths). + bool get isFilled => _paths.isNotEmpty; + + /// Controls input from [HandSignature] and creates smooth signature path. + /// + /// [setup] dynamic setup for every new path. Setup can be also set later. + /// [initialSetup] default setup for each path (ignored if [setup] is provided). + /// + /// [threshold] minimal distance between two points. + /// [smoothRatio] smoothing ratio of curved parts. + /// [velocityRange] controls velocity speed and dampening between points (only Shape and Arc drawing types using this property to control line width). aka how fast si signature drawn.. + /// [pressureRatio] ratio between pressure and velocity. 0.0 = only velocity, 1.0 = only pressure + HandSignatureControl({ + @Deprecated('Use {setup} or {initialSetup}') double threshold = 3.0, + @Deprecated('Use {setup} or {initialSetup}') double smoothRatio = 0.65, + @Deprecated('Use {setup} or {initialSetup}') double velocityRange = 2.0, + @Deprecated('Use {setup} or {initialSetup}') double pressureRatio = 0.0, + SignaturePathSetup Function()? setup, + SignaturePathSetup? initialSetup, + }) { + this.setup = setup ?? + () => + initialSetup ?? + SignaturePathSetup( + threshold: threshold, + smoothRatio: smoothRatio, + velocityRange: velocityRange, + pressureRatio: pressureRatio, + ); + } + + factory HandSignatureControl.fromMap(Map data) => + HandSignatureControl()..import(data); + + /// Sets setup for next Path + void setSetup(SignaturePathSetup setup) => this.setup = () => setup; + + /// Starts new line at given [point]. + void startPath(Offset point, {double? pressure}) { + assert(!hasActivePath); + + _activePath = CubicPath(setup: setup.call()); + + _activePath!.begin( + point, + velocity: _paths.isNotEmpty ? _paths.last._currentVelocity : 0.0, + pressure: pressure, + ); + + _paths.add(_activePath!); + } + + /// Adds [point[ to active path. + void alterPath(Offset point, {double? pressure}) { + assert(hasActivePath); + + _activePath?.add( + point, + pressure: pressure, + ); + + notifyListeners(); + } + + /// Closes active path at given [point]. + void closePath({Offset? point, double? pressure}) { + assert(hasActivePath); + + final valid = _activePath?.end( + point: point, + pressure: pressure, + ); + + if (valid == false) { + _paths.removeLast(); + } + + _activePath = null; + + notifyListeners(); + } + + /// Imports given [paths] and alters current signature data. + @Deprecated('User {addPath}') + void importPath(List paths, [Size? bounds]) => + addPath(paths, bounds); + + /// Imports given [paths] and alters current signature data. + void addPath(List paths, [Size? bounds]) { + //TODO: check bounds + + if (bounds != null) { + if (_areaSize.isEmpty) { + print( + 'Signature: Canvas area is not specified yet. Signature can be out of visible bounds or misplaced.'); + } else if (_areaSize != bounds) { + print( + 'Signature: Canvas area has different size. Signature can be out of visible bounds or misplaced.'); + } + } + + _paths.addAll(paths); + notifyListeners(); + } + + /// Removes last Path. + /// returns removed [CubicPath]. + CubicPath? stepBack() { + if (_paths.isNotEmpty) { + final path = _paths.removeLast(); + notifyListeners(); + + return path; + } + + return null; + } + + /// Clears all data. + void clear() { + _paths.clear(); + + notifyListeners(); + } + + //TODO: Only landscape to landscape mode works correctly now. Add support for orientation switching. + /// Handles canvas size changes. + bool notifyDimension(Size size) { + if (_areaSize == size) { + return false; + } + + if (_areaSize.isEmpty || + _areaSize.width == size.width || + _areaSize.height == size.height) { + _areaSize = size; + return false; + } + + //TODO: iOS device holds pointer during rotation + if (hasActivePath) { + closePath(); + } + + if (!isFilled) { + _areaSize = size; + return false; + } + + //final ratioX = size.width / _areaSize.width; + final ratioY = size.height / _areaSize.height; + final scale = ratioY; + + _areaSize = size; + + _paths.forEach((path) { + path.setScale(scale); + }); + + //TODO: Called during rebuild, so notify must be postponed one frame - should be solved by widget/state + Future.delayed(Duration(), () => notifyListeners()); + + return true; + } + + @Deprecated('User {import}') + void importData(Map data) => import(data); + + /// Expects [data] from [toMap]. + void import(Map data) { + final list = []; + + final v2 = (data['version'] ?? 1) == 2; + final bounds = Size(data['bounds']['width'], data['bounds']['height']); + final paths = data['paths'] as Iterable; + final setups = data['setup'] as Iterable?; + + if (v2) { + assert(setups != null); + assert(paths.length == setups!.length); + } else { + final threshold = data['threshold']; + final smoothRatio = data['smoothRatio']; + final velocityRange = data['velocityRange']; + + setup = () => SignaturePathSetup( + threshold: threshold, + smoothRatio: smoothRatio, + velocityRange: velocityRange, + ); + } + + final count = paths.length; + + for (int i = 0; i < count; i++) { + final points = List.from(paths.elementAt(i)); + final setup = + v2 ? SignaturePathSetup.fromMap(setups!.elementAt(i)) : this.setup(); + + final cp = CubicPath(setup: setup); + + cp.begin(OffsetPoint.fromMap(points[0])); + points.skip(1).forEach((element) => cp.add(OffsetPoint.fromMap(element))); + cp.end(); + + list.add(cp); + } + + addPath(list, bounds); + } + + /// Converts dat to Map (json) + /// Exported data can be restored via [HandSignatureControl.fromMap] factory or via [import] method. + Map toMap() => { + 'version': 2, + 'bounds': { + 'width': _areaSize.width, + 'height': _areaSize.height, + }, + 'paths': + paths.map((p) => p.points.map((p) => p.toMap()).toList()).toList(), + 'setup': paths.map((p) => p.setup.toMap()).toList(), + }; + + /// Converts data to [svg] String. + /// [type] - data structure. + String? toSvg({ + SignatureDrawType type = SignatureDrawType.shape, + int width = 512, + int height = 256, + double border = 0.0, + Color? color, + double? strokeWidth, + double? maxStrokeWidth, + bool fit = false, + }) { + if (!isFilled) { + return null; + } + + params ??= SignaturePaintParams( + color: Colors.black, + strokeWidth: 1.0, + maxStrokeWidth: 10.0, + ); + + color ??= params!.color; + strokeWidth ??= params!.strokeWidth; + maxStrokeWidth ??= params!.maxStrokeWidth; + + final bounds = PathUtil.boundsOf(_offsets, radius: maxStrokeWidth * 0.5); + final fitBox = + bounds.size.scaleToFit(Size(width.toDouble(), height.toDouble())); + final rect = fit + ? Rect.fromLTWH(0.0, 0.0, fitBox.width, fitBox.height) + : Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()); + + final data = PathUtil.fillData( + _cubicLines, + rect, + bound: bounds, + border: maxStrokeWidth + border, + ); + + switch (type) { + case SignatureDrawType.line: + return _exportPathSvg(data, rect.size, color, strokeWidth); + case SignatureDrawType.shape: + return _exportShapeSvg( + data, rect.size, color, strokeWidth, maxStrokeWidth); + case SignatureDrawType.arc: + final arcs = []; + + for (final lines in data) { + for (final line in lines) { + arcs.addAll(line.toArc()); + } + } + + return _exportArcSvg( + arcs, rect.size, color, strokeWidth, maxStrokeWidth); + } + } + + /// Exports [svg] as simple line. + String _exportPathSvg( + List> data, + Size size, + Color color, + double strokeWidth, + ) { + final buffer = StringBuffer(); + buffer.writeln(''); + buffer.writeln( + ''); + buffer.writeln( + ''); + + data.forEach((line) { + buffer.write(''); + }); + + buffer.writeln(''); + buffer.writeln(''); + + return buffer.toString(); + } + + /// Exports [svg] as a lot of arcs. + String _exportArcSvg( + List data, + Size size, + Color color, + double strokeWidth, + double maxStrokeWidth, + ) { + final buffer = StringBuffer(); + buffer.writeln(''); + buffer.writeln( + ''); + buffer.writeln( + ''); + + data.forEach((arc) { + final strokeSize = + strokeWidth + (maxStrokeWidth - strokeWidth) * arc.size; + buffer.writeln( + ''); + }); + + buffer.writeln(''); + buffer.writeln(''); + + return buffer.toString(); + } + + /// Exports [svg] as shape - 4 paths per line. Path is closed and filled with given color. + String _exportShapeSvg( + List> data, + Size size, + Color color, + double strokeWidth, + double maxStrokeWidth, + ) { + final buffer = StringBuffer(); + buffer.writeln(''); + buffer.writeln( + ''); + buffer.writeln(''); + + data.forEach((lines) { + if (lines.length == 1 && lines[0].isDot) { + final dot = lines[0]; + buffer.writeln( + ''); + } else { + final firstLine = lines.first; + final start = + firstLine.start + firstLine.cpsUp(strokeWidth, maxStrokeWidth); + buffer.write(''); + + buffer.writeln( + ''); + buffer.writeln( + ''); + } + }); + + buffer.writeln(''); + buffer.writeln(''); + + return buffer.toString(); + } + + /// Exports data to [Picture]. + /// + /// If [fit] is enabled, the path will be normalized and scaled to fit given [width] and [height]. + Picture? toPicture({ + int width = 512, + int height = 256, + Color? color, + Color? background, + double? strokeWidth, + double? maxStrokeWidth, + HandSignatureDrawer? drawer, + double border = 0.0, + bool fit = false, + }) { + if (!isFilled) { + return null; + } + + final outputArea = + Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()); + + params ??= SignaturePaintParams( + color: Colors.black, + strokeWidth: 1.0, + maxStrokeWidth: 10.0, + ); + + maxStrokeWidth ??= params!.maxStrokeWidth; + + final bounds = PathUtil.boundsOf(_offsets, radius: maxStrokeWidth * 0.5); + + final data = PathUtil.fillData( + _cubicLines, + outputArea, + bound: fit + ? bounds + : bounds.size + .scaleToFit(Size(width.toDouble(), height.toDouble())) + .toRect(), + border: maxStrokeWidth + border, + ); + + int i = 0; + final painter = PathSignaturePainter( + paths: paths + .map((e) => CubicPath(setup: e.setup).._lines.addAll(data[i++])) + .toList(), + drawer: drawer ?? + ArcSignatureDrawer( + color: color ?? params!.color, + width: strokeWidth ?? params!.strokeWidth, + maxWidth: maxStrokeWidth, + ), + ); + + final recorder = PictureRecorder(); + final canvas = Canvas( + recorder, + outputArea, + ); + + if (background != null) { + canvas.drawColor(background, BlendMode.src); + } + + painter.paint(canvas, outputArea.size); + + return recorder.endRecording(); + } + + /// Exports data to raw image. + /// + /// If [fit] is enabled, the path will be normalized and scaled to fit given [width] and [height]. + Future toImage({ + int width = 512, + int height = 256, + Color? color, + Color? background, + double? strokeWidth, + double? maxStrokeWidth, + HandSignatureDrawer? drawer, + double border = 0.0, + ImageByteFormat format = ImageByteFormat.png, + bool fit = false, + }) async { + final image = await toPicture( + width: width, + height: height, + color: color, + background: background, + strokeWidth: strokeWidth, + maxStrokeWidth: maxStrokeWidth, + drawer: drawer, + border: border, + fit: fit, + )?.toImage(width, height); + + if (image == null) { + return null; + } + + return image.toByteData(format: format); + } + + /// Currently checks only equality of [paths]. + bool equals(HandSignatureControl other) { + if (paths.length == other.paths.length) { + for (int i = 0; i < paths.length; i++) { + if (!paths[i].equals(other.paths[i])) { + return false; + } + } + + return true; + } + + return false; + } + + @override + void dispose() { + _paths.clear(); + _activePath = null; + + super.dispose(); + } +} diff --git a/hand_signature/lib/src/signature_drawer.dart b/hand_signature/lib/src/signature_drawer.dart new file mode 100644 index 0000000..55f559b --- /dev/null +++ b/hand_signature/lib/src/signature_drawer.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; + +import '../signature.dart'; +import 'utils.dart'; + +/// An abstract base class for custom signature drawing logic. +/// +/// Subclasses must implement the [paint] method to define how a signature path is rendered on a canvas. +abstract class HandSignatureDrawer { + /// Creates a [HandSignatureDrawer] instance. + const HandSignatureDrawer(); + + /// Paints the given [paths] onto the [canvas]. + /// + /// [canvas] The canvas to draw on. + /// [size] The size of the canvas. + /// [paths] A list of [CubicPath] objects representing the signature to be drawn. + void paint(Canvas canvas, Size size, List paths); +} + +/// A concrete implementation of [HandSignatureDrawer] that draws signature as simple lines. +class LineSignatureDrawer extends HandSignatureDrawer { + /// The color used to paint the lines. + final Color color; + + /// The stroke width of the lines. + final double width; + + /// Creates a [LineSignatureDrawer] with the specified [width] and [color]. + const LineSignatureDrawer({ + this.width = 1.0, + this.color = Colors.black, + }); + + @override + void paint(Canvas canvas, Size size, List paths) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..strokeWidth = width; + + for (final path in paths) { + if (path.isFilled) { + canvas.drawPath(PathUtil.toLinePath(path.lines), paint); + } + } + } +} + +/// A concrete implementation of [HandSignatureDrawer] that draws signatures as arcs, +/// with varying width based on the arc's size property. +class ArcSignatureDrawer extends HandSignatureDrawer { + /// The color used to paint the arcs. + final Color color; + + /// The minimal stroke width of the arcs. + final double width; + + /// The maximal stroke width of the arcs. + final double maxWidth; + + /// Creates an [ArcSignatureDrawer] with the specified [width], [maxWidth], and [color]. + const ArcSignatureDrawer({ + this.width = 1.0, + this.maxWidth = 10.0, + this.color = Colors.black, + }); + + @override + void paint(Canvas canvas, Size size, List paths) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..strokeWidth = width; + + for (final path in paths) { + final arcs = path.toArcs(); + for (final arc in arcs) { + paint.strokeWidth = width + (maxWidth - width) * arc.size; + canvas.drawPath(arc.path, paint); + } + } + } +} + +/// A concrete implementation of [HandSignatureDrawer] that draws signature as filled Path. +class ShapeSignatureDrawer extends HandSignatureDrawer { + /// The color used to fill the shapes. + final Color color; + + /// The base width of the shape. + final double width; + + /// The maximum width of the shape. + final double maxWidth; + + /// Creates a [ShapeSignatureDrawer] with the specified [width], [maxWidth], and [color]. + const ShapeSignatureDrawer({ + this.width = 1.0, + this.maxWidth = 10.0, + this.color = Colors.black, + }); + + @override + void paint(Canvas canvas, Size size, List paths) { + final paint = Paint() + ..color = color + ..strokeWidth = 0.0; // Stroke width is handled by the shape path itself + + for (final path in paths) { + if (path.isFilled) { + if (path.isDot) { + // If it's a dot, draw a circle + canvas.drawCircle(path.lines[0], path.lines[0].startRadius(width, maxWidth), paint); + } else { + // Otherwise, draw the filled shape path + canvas.drawPath(PathUtil.toShapePath(path.lines, width, maxWidth), paint); + + // Draw circles at the start and end of the path for a smoother look + final first = path.lines.first; + final last = path.lines.last; + + canvas.drawCircle(first.start, first.startRadius(width, maxWidth), paint); + canvas.drawCircle(last.end, last.endRadius(width, maxWidth), paint); + } + } + } + } +} + +/// A [HandSignatureDrawer] that dynamically selects the drawing type based on +/// arguments provided in the [CubicPath]'s setup. +class DynamicSignatureDrawer extends HandSignatureDrawer { + final SignatureDrawType type; + + /// The color used to paint the arcs. + final Color color; + + /// The minimal stroke width of the arcs. + final double width; + + /// The maximal stroke width of the arcs. + final double maxWidth; + + const DynamicSignatureDrawer({ + this.type = SignatureDrawType.shape, + this.width = 1.0, + this.maxWidth = 10.0, + this.color = Colors.black, + }); + + @override + void paint(Canvas canvas, Size size, List paths) { + for (final path in paths) { + // Retrieve drawing parameters from path arguments, with fallbacks + final type = path.setup.args?['type'] ?? this.type.name; + final color = Color(path.setup.args?['color'] ?? this.color.toHex32()); + final width = path.setup.args?['width'] ?? this.width; + final maxWidth = path.setup.args?['max_width'] ?? this.maxWidth; + + HandSignatureDrawer drawer; + + // Select the appropriate drawer based on the 'type' argument + switch (type) { + case 'line': + drawer = LineSignatureDrawer(color: color, width: width); + break; + case 'arc': + drawer = ArcSignatureDrawer(color: color, width: width, maxWidth: maxWidth); + break; + case 'shape': + drawer = ShapeSignatureDrawer(color: color, width: width, maxWidth: maxWidth); + break; + default: + // Default to ShapeSignatureDrawer if type is unknown or not provided + drawer = ShapeSignatureDrawer(color: color, width: width, maxWidth: maxWidth); + } + + // Paint the current path using the selected drawer + drawer.paint(canvas, size, [path]); + } + } +} + +/// A [HandSignatureDrawer] that combines multiple drawers, allowing for complex +/// drawing effects by applying each drawer in sequence. +class MultiSignatureDrawer extends HandSignatureDrawer { + /// The collection of [HandSignatureDrawer]s to be applied. + final Iterable drawers; + + /// Creates a [MultiSignatureDrawer] with the given [drawers]. + const MultiSignatureDrawer({required this.drawers}); + + @override + void paint(Canvas canvas, Size size, List paths) { + for (final drawer in drawers) { + drawer.paint(canvas, size, paths); + } + } +} diff --git a/hand_signature/lib/src/signature_paint.dart b/hand_signature/lib/src/signature_paint.dart new file mode 100644 index 0000000..4fd8453 --- /dev/null +++ b/hand_signature/lib/src/signature_paint.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +import '../signature.dart'; + +/// A [StatefulWidget] that uses [CustomPaint] to render a hand signature. +/// It rebuilds automatically whenever the signature data managed by [HandSignatureControl] changes. +/// +/// This widget is typically used internally by [HandSignature] and [HandSignatureView]. +class HandSignaturePaint extends StatefulWidget { + /// The controller that manages the signature paths and notifies listeners of changes. + final HandSignatureControl control; + + /// The drawer responsible for rendering the signature paths on the canvas. + final HandSignatureDrawer drawer; + + /// Optional callback that is invoked when the canvas size changes. + /// + /// TODO: This callback should ideally be handled within the State of this widget + /// or by the [HandSignatureControl] itself, rather than being exposed here. + final bool Function(Size size)? onSize; + + /// Creates a [HandSignaturePaint] widget. + /// + /// [key] Controls how one widget replaces another widget in the tree. + /// [control] The [HandSignatureControl] instance that provides the signature data. + /// [drawer] The [HandSignatureDrawer] instance that defines how the signature is painted. + /// [onSize] An optional callback for canvas size changes. + const HandSignaturePaint({ + Key? key, + required this.control, + required this.drawer, + this.onSize, + }) : super(key: key); + + @override + _HandSignaturePaintState createState() => _HandSignaturePaintState(); +} + +/// The state class for [HandSignaturePaint]. +/// +/// This state subscribes to the [HandSignatureControl] to listen for changes +/// in signature data and triggers a rebuild of the widget when updates occur. +class _HandSignaturePaintState extends State { + @override + void initState() { + super.initState(); + // Add a listener to the control to trigger a rebuild on data changes. + widget.control.addListener(_updateState); + } + + /// Callback method to trigger a widget rebuild. + void _updateState() { + setState(() {}); + } + + @override + void didUpdateWidget(HandSignaturePaint oldWidget) { + super.didUpdateWidget(oldWidget); + // If the control instance changes, update the listener. + if (oldWidget.control != widget.control) { + oldWidget.control.removeListener(_updateState); + widget.control.addListener(_updateState); + } + } + + @override + Widget build(BuildContext context) { + // Use CustomPaint to draw the signature using PathSignaturePainter. + return CustomPaint( + painter: PathSignaturePainter( + paths: widget.control.paths, + drawer: widget.drawer, + onSize: widget.onSize, + ), + ); + } + + @override + void dispose() { + // Remove the listener when the widget is disposed to prevent memory leaks. + widget.control.removeListener(_updateState); + super.dispose(); + } +} diff --git a/hand_signature/lib/src/signature_painter.dart b/hand_signature/lib/src/signature_painter.dart new file mode 100644 index 0000000..e300af2 --- /dev/null +++ b/hand_signature/lib/src/signature_painter.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; + +import '../signature.dart'; + +/// Defines the different types of drawing styles for a signature path. +enum SignatureDrawType { + /// Draws the signature as a simple line with a constant stroke width. + /// This is the most basic and performant drawing style. + line, + + /// Draws the signature as a series of small arcs. + /// This style can produce a visually appealing result but might have + /// a higher performance cost due to the large number of individual arcs drawn. + arc, + + /// Draws the signature by creating a closed shape for each segment of the line and filling it. + /// This method generally provides a good balance between visual quality and performance, + /// resulting in a smooth, filled signature appearance. + shape, +} + +/// A [CustomPainter] responsible for rendering [CubicPath]s onto a canvas. +/// This painter is used internally by the signature drawing widgets. +class PathSignaturePainter extends CustomPainter { + /// The list of [CubicPath]s that need to be painted. + final List paths; + + /// The [HandSignatureDrawer] instance that defines the actual drawing logic. + final HandSignatureDrawer drawer; + + /// Optional callback that is invoked when the canvas size changes. + /// + /// TODO: This callback should ideally be handled within the widget's state + /// or by the [HandSignatureControl] itself. + final bool Function(Size size)? onSize; + + /// Creates a [PathSignaturePainter]. + /// + /// [paths] The list of signature paths to draw. + /// [drawer] The drawer that will perform the actual painting. + /// [onSize] An optional callback for canvas size changes. + const PathSignaturePainter({ + required this.paths, + required this.drawer, + this.onSize, + }); + + @override + void paint(Canvas canvas, Size size) { + // TODO: This size handling logic should be moved to the widget/state. + if (onSize != null) { + if (onSize!.call(size)) { + return; + } + } + + // If there are no paths, nothing to draw. + if (paths.isEmpty) { + return; + } + + // Delegate the actual painting to the provided drawer. + drawer.paint(canvas, size, paths); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + // Always repaint to ensure the latest signature is displayed. + // A more optimized approach might compare old and new paths. + return true; + } +} + +/// A [CustomPainter] used for debugging purposes, specifically to visualize +/// the control points and segments of a signature path. +class DebugSignaturePainterCP extends CustomPainter { + /// The [HandSignatureControl] instance providing the signature data. + final HandSignatureControl control; + + /// Whether to draw all control points. + final bool cp; + + /// Whether to draw control points related to the start of segments. + final bool cpStart; + + /// Whether to draw control points related to the end of segments. + final bool cpEnd; + + /// Whether to draw dots at the control points and segment ends. + final bool dot; + + /// The color used for drawing the debug elements. + final Color color; + + /// Creates a [DebugSignaturePainterCP]. + /// + /// [control] The signature control providing the data to debug. + /// [cp] Whether to draw all control points. + /// [cpStart] Whether to draw control points at the start of segments. + /// [cpEnd] Whether to draw control points at the end of segments. + /// [dot] Whether to draw dots at the control points and segment ends. + /// [color] The color for the debug drawings. + const DebugSignaturePainterCP({ + required this.control, + this.cp = false, + this.cpStart = true, + this.cpEnd = true, + this.dot = true, + this.color = Colors.red, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..strokeWidth = 1.0; + + // Iterate through each line segment in the control's paths. + control.lines.forEach((line) { + // Draw lines and dots for start control points if enabled. + if (cpStart) { + canvas.drawLine(line.start, line.cpStart, paint); + if (dot) { + canvas.drawCircle(line.cpStart, 1.0, paint); + canvas.drawCircle(line.start, 1.0, paint); + } + } else if (cp) { + // Draw only the control point dot if cpStart is false but cp is true. + canvas.drawCircle(line.cpStart, 1.0, paint); + } + + // Draw lines and dots for end control points if enabled. + if (cpEnd) { + canvas.drawLine(line.end, line.cpEnd, paint); + if (dot) { + canvas.drawCircle(line.cpEnd, 1.0, paint); + } + } + }); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + // Always repaint to show the latest debug information. + return true; + } +} diff --git a/hand_signature/lib/src/signature_view.dart b/hand_signature/lib/src/signature_view.dart new file mode 100644 index 0000000..9d30362 --- /dev/null +++ b/hand_signature/lib/src/signature_view.dart @@ -0,0 +1,217 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import '../signature.dart'; + +typedef _GestureEvent = Function(Offset position, double pressure); + +/// A widget that provides a canvas for drawing hand signatures. +/// It combines [HandSignaturePaint] for rendering and [RawGestureDetector] for input handling, +/// sending gesture events to a [HandSignatureControl]. +class HandSignature extends StatelessWidget { + /// The controller that manages the creation and manipulation of signature paths. + final HandSignatureControl control; + + /// @Deprecated('This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the drawing type and style.') + /// The type of signature path to draw. + @Deprecated( + 'This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the drawing type and style.') + final SignatureDrawType type; + + /// @Deprecated('This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the drawing color.') + /// The single color used for painting the signature. + @Deprecated( + 'This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the drawing color.') + final Color color; + + /// @Deprecated('This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the minimal stroke width.') + /// The minimal stroke width of the signature path. + @Deprecated( + 'This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the minimal stroke width.') + final double width; + + /// @Deprecated('This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the maximal stroke width.') + /// The maximal stroke width of the signature path. + @Deprecated( + 'This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the maximal stroke width.') + final double maxWidth; + + /// The [HandSignatureDrawer] responsible for rendering the signature. + /// If `null`, a default drawer will be created based on the deprecated `type`, `color`, `width`, and `maxWidth` properties. + final HandSignatureDrawer? drawer; + + /// The set of [PointerDeviceKind]s that this widget should recognize. + /// + /// For example, to only accept stylus input: + /// ```dart + /// supportedDevices: { + /// PointerDeviceKind.stylus, + /// } + /// ``` + /// If `null`, it accepts input from all pointer device types. + final Set? supportedDevices; + + /// Optional callback function invoked when a new signature path drawing starts (pointer down event). + final VoidCallback? onPointerDown; + + /// Optional callback function invoked when a signature path drawing ends (pointer up or cancel event). + final VoidCallback? onPointerUp; + + /// Creates a [HandSignature] widget. + /// + /// [key] Controls how one widget replaces another widget in the tree. + /// [control] The [HandSignatureControl] instance to manage the signature data. + /// [type] The deprecated drawing type for the signature. + /// [color] The deprecated color for the signature. + /// [width] The deprecated minimal width for the signature. + /// [maxWidth] The deprecated maximal width for the signature. + /// [drawer] The custom drawer to use for rendering the signature. + /// [onPointerDown] Callback for when drawing starts. + /// [onPointerUp] Callback for when drawing ends. + /// [supportedDevices] The set of pointer device types to recognize. + const HandSignature({ + Key? key, + required this.control, + @Deprecated( + 'This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the drawing type and style.') + this.type = SignatureDrawType.shape, + @Deprecated( + 'This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the drawing color.') + this.color = Colors.black, + @Deprecated( + 'This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the minimal stroke width.') + this.width = 1.0, + @Deprecated( + 'This property is deprecated since 3.1.0. Use the `drawer` property instead to specify the maximal stroke width.') + this.maxWidth = 10.0, + this.drawer, + this.onPointerDown, + this.onPointerUp, + this.supportedDevices, + }) : super(key: key); + + void _startPath(Offset point, double pressure) { + if (!control.hasActivePath) { + onPointerDown?.call(); + control.startPath(point, pressure: pressure); + } + } + + void _endPath(Offset point, double pressure) { + if (control.hasActivePath) { + control.closePath(pressure: pressure); + onPointerUp?.call(); + } + } + + @override + Widget build(BuildContext context) { + control.params = SignaturePaintParams( + color: color, + strokeWidth: width, + maxStrokeWidth: maxWidth, + ); + + return ClipRRect( + child: RawGestureDetector( + gestures: { + _SingleGestureRecognizer: + GestureRecognizerFactoryWithHandlers<_SingleGestureRecognizer>( + () => _SingleGestureRecognizer( + debugOwner: this, supportedDevices: supportedDevices), + (instance) { + instance.onStart = + (position, pressure) => _startPath(position, pressure); + instance.onUpdate = (position, pressure) => + control.alterPath(position, pressure: pressure); + instance.onEnd = + (position, pressure) => _endPath(position, pressure); + }, + ), + }, + child: HandSignaturePaint( + control: control, + drawer: drawer ?? + switch (type) { + SignatureDrawType.line => + LineSignatureDrawer(color: color, width: width), + SignatureDrawType.arc => ArcSignatureDrawer( + color: color, width: width, maxWidth: maxWidth), + SignatureDrawType.shape => ShapeSignatureDrawer( + color: color, width: width, maxWidth: maxWidth), + }, + onSize: control.notifyDimension, + ), + ), + ); + } +} + +/// A custom [GestureRecognizer] that processes only a single input pointer +/// for signature drawing. It extends [OneSequenceGestureRecognizer] to ensure +/// that only one gesture is recognized at a time. +class _SingleGestureRecognizer extends OneSequenceGestureRecognizer { + @override + String get debugDescription => 'single_gesture_recognizer'; + + /// Callback function for when a pointer starts interacting with the widget. + _GestureEvent? onStart; + + /// Callback function for when a pointer moves while interacting with the widget. + _GestureEvent? onUpdate; + + /// Callback function for when a pointer stops interacting with the widget. + _GestureEvent? onEnd; + + /// A flag indicating whether a pointer is currently active (down). + bool pointerActive = false; + + /// Creates a [_SingleGestureRecognizer]. + /// + /// [debugOwner] The object that is debugging this recognizer. + /// [supportedDevices] The set of [PointerDeviceKind]s that this recognizer should respond to. + /// If `null`, it defaults to all available pointer device kinds. + _SingleGestureRecognizer({ + super.debugOwner, + Set? supportedDevices, + }) : super( + supportedDevices: + supportedDevices ?? PointerDeviceKind.values.toSet(), + ); + + @override + void addAllowedPointer(PointerDownEvent event) { + // Only allow a new pointer if no other pointer is currently active. + if (pointerActive) { + return; + } + // Start tracking the pointer. + startTrackingPointer(event.pointer, event.transform); + } + + @override + void handleEvent(PointerEvent event) { + // Handle different types of pointer events. + if (event is PointerMoveEvent) { + // If it's a move event, call the onUpdate callback. + onUpdate?.call(event.localPosition, event.pressure); + } else if (event is PointerDownEvent) { + // If it's a down event, set pointer as active and call onStart. + pointerActive = true; + onStart?.call(event.localPosition, event.pressure); + } else if (event is PointerUpEvent) { + // If it's an up event, set pointer as inactive and call onEnd. + pointerActive = false; + onEnd?.call(event.localPosition, event.pressure); + } else if (event is PointerCancelEvent) { + // If the pointer interaction is cancelled, set pointer as inactive and call onEnd. + pointerActive = false; + onEnd?.call(event.localPosition, event.pressure); + } + } + + @override + void didStopTrackingLastPointer(int pointer) { + // No specific action needed when the last pointer stops tracking. + } +} diff --git a/hand_signature/lib/src/utils.dart b/hand_signature/lib/src/utils.dart new file mode 100644 index 0000000..99653c7 --- /dev/null +++ b/hand_signature/lib/src/utils.dart @@ -0,0 +1,634 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import '../signature.dart'; + +/// A constant representing 2 * PI (360 degrees in radians). +const pi2 = math.pi * 2.0; + +/// Extension methods for [Color] to provide utility functions. +extension ColorEx on Color { + /// Returns the hexadecimal string representation of this color, + /// excluding the alpha component (e.g., '#RRGGBB'). + String get hexValue => + '#${toHex32().toRadixString(16)}'.replaceRange(1, 3, ''); + + /// {toARGB32} was introduced in Flutter 3.28 + int toHex32() { + int floatToInt8(double x) { + return (x * 255.0).round() & 0xff; + } + + return floatToInt8(a) << 24 | + floatToInt8(r) << 16 | + floatToInt8(g) << 8 | + floatToInt8(b) << 0; + } +} + +/// Extension methods for [Offset] to provide vector and geometric utility functions. +extension OffsetEx on Offset { + /// Calculates the component-wise distance (difference) between this offset and [other]. + /// Returns an [Offset] representing (other.dx - this.dx, other.dy - this.dy). + Offset axisDistanceTo(Offset other) => other - this; + + /// Calculates the Euclidean distance between this offset and [other]. + double distanceTo(Offset other) { + final len = axisDistanceTo(other); + return math.sqrt(len.dx * len.dx + len.dy * len.dy); + } + + /// Calculates the angle (in radians) from this offset to [other] relative to the positive x-axis. + double angleTo(Offset other) { + final len = axisDistanceTo(other); + return math.atan2(len.dy, len.dx); + } + + /// Calculates the unit vector (direction) from this offset to [other]. + /// Returns an [Offset] representing the normalized direction. + /// If the distance is zero, returns an [Offset] of (0,0). + Offset directionTo(Offset other) { + final len = axisDistanceTo(other); + final m = math.sqrt(len.dx * len.dx + len.dy * len.dy); + return Offset(m == 0 ? 0 : (len.dx / m), m == 0 ? 0 : (len.dy / m)); + } + + /// Rotates this offset by [radians] around the origin (0,0). + /// Returns a new [Offset] representing the rotated point. + Offset rotate(double radians) { + final s = math.sin(radians); + final c = math.cos(radians); + final x = dx * c - dy * s; + final y = dx * s + dy * c; + return Offset(x, y); + } + + /// Rotates this offset by [radians] around a specified [center] point. + /// Returns a new [Offset] representing the rotated point. + Offset rotateAround(Offset center, double radians) { + return (this - center).rotate(radians) + center; + } +} + +/// Extension methods for [Path] to provide simplified drawing commands. +extension PathEx on Path { + /// Moves the current point of the path to the given [offset]. + void start(Offset offset) => moveTo(offset.dx, offset.dy); + + /// Adds a cubic Bezier curve segment to the path. + /// + /// [cpStart] The first control point. + /// [cpEnd] The second control point. + /// [end] The end point of the curve. + void cubic(Offset cpStart, Offset cpEnd, Offset end) => + cubicTo(cpStart.dx, cpStart.dy, cpEnd.dx, cpEnd.dy, end.dx, end.dy); + + /// Adds a straight line segment from the current point to the given [offset]. + void line(Offset offset) => lineTo(offset.dx, offset.dy); +} + +/// Extension methods for [Size] to provide utility functions. +extension SizeExt on Size { + /// Scales this size down to fit within [other] size while maintaining aspect ratio. + /// Returns a new [Size] that fits within [other]. + Size scaleToFit(Size other) { + final scale = math.min( + other.width / width, + other.height / height, + ); + + return this * scale; + } + + Rect toRect() => Rect.fromLTRB(0.0, 0.0, width, height); +} + +/// A utility class providing static methods for common path manipulation and geometric calculations. +/// This includes bounding box calculations, transformations (translate, scale, normalize), +/// and conversions between different path representations. +/// +/// TODO: Consider refactoring and cleaning up this class for better organization and clarity. +class PathUtil { + /// Private constructor to prevent direct instantiation of this utility class. + const PathUtil._(); + + /// Calculates the bounding box (minimum [Rect]) for a list of [Offset] points. + /// + /// [data] The list of [Offset] points. + /// [minSize] The minimum width/height for the bounding box. If the calculated + /// size is smaller, it will be expanded to this minimum. + /// [radius] An additional padding to add around the calculated bounds. + /// Returns a [Rect] representing the bounding box. + /// Calculates the bounding box (minimum [Rect]) for a list of [Offset] points. + /// + /// [data] The list of [Offset] points. + /// [minSize] The minimum width/height for the bounding box. If the calculated + /// size is smaller, it will be expanded to this minimum. + /// [radius] An additional padding to add around the calculated bounds. + /// Returns a [Rect] representing the bounding box. + static Rect bounds(List data, + {double minSize = 2.0, double radius = 0.0}) { + double left = data[0].dx; + double top = data[0].dy; + double right = data[0].dx; + double bottom = data[0].dy; + + for (final point in data) { + final x = point.dx; + final y = point.dy; + + if (x < left) { + left = x; + } else if (x > right) { + right = x; + } + + if (y < top) { + top = y; + } else if (y > bottom) { + bottom = y; + } + } + + final hSize = right - left; + final vSize = bottom - top; + + if (hSize < minSize) { + final dif = (minSize - hSize) * 0.5; + left -= dif; + right += dif; + } + + if (vSize < minSize) { + final dif = (minSize - vSize) * 0.5; + top -= dif; + bottom += dif; + } + + return Rect.fromLTRB( + left - radius, top - radius, right + radius, bottom + radius); + } + + /// Calculates the bounding box (minimum [Rect]) for a list of lists of [Offset] points. + /// This is useful for finding the overall bounds of multiple paths. + /// + /// [data] The list of lists of [Offset] points. + /// [minSize] The minimum width/height for the bounding box. + /// [radius] An additional padding to add around the calculated bounds. + /// Returns a [Rect] representing the combined bounding box. + /// Calculates the bounding box (minimum [Rect]) for a list of lists of [Offset] points. + /// This is useful for finding the overall bounds of multiple paths. + /// + /// [data] The list of lists of [Offset] points. + /// [minSize] The minimum width/height for the bounding box. + /// [radius] An additional padding to add around the calculated bounds. + /// Returns a [Rect] representing the combined bounding box. + static Rect boundsOf(List> data, + {double minSize = 2.0, double radius = 0.0}) { + double left = data[0][0].dx; + double top = data[0][0].dy; + double right = data[0][0].dx; + double bottom = data[0][0].dy; + + for (final set in data) { + for (final point in set) { + final x = point.dx; + final y = point.dy; + + if (x < left) { + left = x; + } else if (x > right) { + right = x; + } + + if (y < top) { + top = y; + } else if (y > bottom) { + bottom = y; + } + } + } + + final hSize = right - left; + final vSize = bottom - top; + + if (hSize < minSize) { + final dif = (minSize - hSize) * 0.5; + left -= dif; + right += dif; + } + + if (vSize < minSize) { + final dif = (minSize - vSize) * 0.5; + top -= dif; + bottom += dif; + } + + return Rect.fromLTRB( + left - radius, top - radius, right + radius, bottom + radius); + } + + /// Translates a list of [Offset] points by a given [location] offset. + /// + /// [data] The list of points to translate. + /// [location] The offset by which to translate the points. + /// Returns a new list of translated points. + /// Translates a list of [Offset] points by a given [location] offset. + /// + /// [data] The list of points to translate. + /// [location] The offset by which to translate the points. + /// Returns a new list of translated points. + static List translate(List data, Offset location) { + final output = []; + for (final point in data) { + output.add(point.translate(location.dx, location.dy) as T); + } + return output; + } + + /// Translates a list of lists of [Offset] points by a given [location] offset. + /// + /// [data] The list of lists of points to translate. + /// [location] The offset by which to translate the points. + /// Returns a new list of lists of translated points. + /// Translates a list of lists of [Offset] points by a given [location] offset. + /// + /// [data] The list of lists of points to translate. + /// [location] The offset by which to translate the points. + /// Returns a new list of lists of translated points. + static List> translateData( + List> data, Offset location) { + final output = >[]; + for (final set in data) { + output.add(translate(set, location)); + } + return output; + } + + /// Scales a list of [Offset] points by a given [ratio]. + /// + /// [data] The list of points to scale. + /// [ratio] The scaling factor. + /// Returns a new list of scaled points. + /// Scales a list of [Offset] points by a given [ratio]. + /// + /// [data] The list of points to scale. + /// [ratio] The scaling factor. + /// Returns a new list of scaled points. + static List scale(List data, double ratio) { + final output = []; + for (final point in data) { + output.add(point.scale(ratio, ratio) as T); + } + return output; + } + + /// Scales a list of lists of [Offset] points by a given [ratio]. + /// + /// [data] The list of lists of points to scale. + /// [ratio] The scaling factor. + /// Returns a new list of lists of scaled points. + /// Scales a list of lists of [Offset] points by a given [ratio]. + /// + /// [data] The list of lists of points to scale. + /// [ratio] The scaling factor. + /// Returns a new list of lists of scaled points. + static List> scaleData( + List> data, double ratio) { + final output = >[]; + for (final set in data) { + output.add(scale(set, ratio)); + } + return output; + } + + /// Normalizes a list of [Offset] points to a unit square (0-1 range) + /// based on their bounding box. + /// + /// [data] The list of points to normalize. + /// [bound] Optional pre-calculated bounding box. If null, it will be calculated. + /// Returns a new list of normalized points. + /// Normalizes a list of [Offset] points to a unit square (0-1 range) + /// based on their bounding box. + /// + /// [data] The list of points to normalize. + /// [bound] Optional pre-calculated bounding box. If null, it will be calculated. + /// Returns a new list of normalized points. + static List normalize(List data, {Rect? bound}) { + bound ??= bounds(data); + return scale( + translate(data, -bound.topLeft), + 1.0 / math.max(bound.width, bound.height), + ); + } + + /// Normalizes a list of lists of [Offset] points to a unit square (0-1 range) + /// based on their combined bounding box. + /// + /// [data] The list of lists of points to normalize. + /// [bound] Optional pre-calculated combined bounding box. If null, it will be calculated. + /// Returns a new list of lists of normalized points. + /// Normalizes a list of lists of [Offset] points to a unit square (0-1 range) + /// based on their combined bounding box. + /// + /// [data] The list of lists of points to normalize. + /// [bound] Optional pre-calculated combined bounding box. If null, it will be calculated. + /// Returns a new list of lists of normalized points. + static List> normalizeData(List> data, + {Rect? bound}) { + bound ??= boundsOf(data); + final ratio = 1.0 / math.max(bound.width, bound.height); + return scaleData( + translateData(data, -bound.topLeft), + ratio, + ); + } + + /// Fills a given [rect] with the scaled and translated [data] points, + /// ensuring they fit within the rectangle with an optional [border]. + /// + /// [data] The list of points to fill. + /// [rect] The target rectangle to fill. + /// [radius] An additional radius to consider for bounding box calculation. + /// [bound] Optional pre-calculated bounding box for the data. + /// [border] The border size to apply around the filled content. + /// Returns a new list of transformed points. + /// Fills a given [rect] with the scaled and translated [data] points, + /// ensuring they fit within the rectangle with an optional [border]. + /// + /// [data] The list of points to fill. + /// [rect] The target rectangle to fill. + /// [radius] An additional radius to consider for bounding box calculation. + /// [bound] Optional pre-calculated bounding box for the data. + /// [border] The border size to apply around the filled content. + /// Returns a new list of transformed points. + static List fill(List data, Rect rect, + {double radius = 0.0, Rect? bound, double border = 32.0}) { + bound ??= bounds(data, radius: radius); + border *= 2.0; + + final outputSize = Size(rect.width - border, rect.height - border); + final sourceSize = Size(bound.width, bound.height); + Size destinationSize; + + final wr = outputSize.width / sourceSize.width; + final hr = outputSize.height / sourceSize.height; + + if (wr < hr) { + //scale by width + destinationSize = Size(outputSize.width, sourceSize.height * wr); + } else { + //scale by height + destinationSize = Size(sourceSize.width * hr, outputSize.height); + } + + final borderSize = Offset(outputSize.width - destinationSize.width + border, + outputSize.height - destinationSize.height + border) * + 0.5; + + return translate( + scale( + normalize(data, bound: bound), + math.max(destinationSize.width, destinationSize.height), + ), + borderSize, + ); + } + + /// Fills a given [rect] with the scaled and translated list of lists of [data] points, + /// ensuring they fit within the rectangle with an optional [border]. + /// + /// [data] The list of lists of points to fill. + /// [rect] The target rectangle to fill. + /// [bound] Optional pre-calculated combined bounding box for the data. + /// [border] The border size to apply around the filled content. + /// Returns a new list of lists of transformed points. + /// Fills a given [rect] with the scaled and translated list of lists of [data] points, + /// ensuring they fit within the rectangle with an optional [border]. + /// + /// [data] The list of lists of points to fill. + /// [rect] The target rectangle to fill. + /// [bound] Optional pre-calculated combined bounding box for the data. + /// [border] The border size to apply around the filled content. + /// Returns a new list of lists of transformed points. + static List> fillData(List> data, Rect rect, + {Rect? bound, double? border}) { + bound ??= boundsOf(data); + border ??= 4.0; + + final outputSize = rect.size; + final sourceSize = bound; + Size destinationSize; + + if (outputSize.width / outputSize.height > + sourceSize.width / sourceSize.height) { + destinationSize = Size( + sourceSize.width * outputSize.height / sourceSize.height, + outputSize.height); + } else { + destinationSize = Size(outputSize.width, + sourceSize.height * outputSize.width / sourceSize.width); + } + + destinationSize = Size(destinationSize.width - border * 2.0, + destinationSize.height - border * 2.0); + final borderSize = Offset(rect.width - destinationSize.width, + rect.height - destinationSize.height) * + 0.5; + + return translateData( + scaleData( + normalizeData(data, bound: bound), + math.max(destinationSize.width, destinationSize.height), + ), + borderSize); + } + + /// Converts a list of [Offset] points into a [Path] object by connecting them with lines. + /// + /// [points] The list of points to convert. + /// Returns a [Path] representing the connected points. + /// Converts a list of [Offset] points into a [Path] object by connecting them with lines. + /// + /// [points] The list of points to convert. + /// Returns a [Path] representing the connected points. + static Path toPath(List points) { + final path = Path(); + if (points.isNotEmpty) { + path.moveTo(points[0].dx, points[0].dy); + for (final point in points) { + path.lineTo(point.dx, point.dy); + } + } + return path; + } + + /// Converts a list of lists of [Offset] points into a list of [Path] objects. + /// + /// [data] The list of lists of points to convert. + /// Returns a list of [Path] objects. + /// Converts a list of lists of [Offset] points into a list of [Path] objects. + /// + /// [data] The list of lists of points to convert. + /// Returns a list of [Path] objects. + static List toPaths(List> data) { + final paths = []; + for (final line in data) { + paths.add(toPath(line)); + } + return paths; + } + + /// Calculates the combined bounding box for a list of [Path] objects. + /// + /// [data] The list of [Path] objects. + /// Returns a [Rect] representing the combined bounding box. + /// Calculates the combined bounding box for a list of [Path] objects. + /// + /// [data] The list of [Path] objects. + /// Returns a [Rect] representing the combined bounding box. + static Rect pathBounds(List data) { + Rect init = data[0].getBounds(); + + double left = init.left; + double top = init.top; + double right = init.right; + double bottom = init.bottom; + + for (final path in data) { + final bound = path.getBounds(); + + left = math.min(left, bound.left); + top = math.min(top, bound.top); + right = math.max(right, bound.right); + bottom = math.max(bottom, bound.bottom); + } + + return Rect.fromLTRB(left, top, right, bottom); + } + + /// Scales a single [Path] object by a given [ratio]. + /// + /// [data] The [Path] to scale. + /// [ratio] The scaling factor. + /// Returns a new, scaled [Path]. + static Path scalePath(Path data, double ratio) { + final transform = Matrix4.identity(); + transform.scale(ratio, ratio); + return data.transform(transform.storage); + } + + /// Scales a list of [Path] objects by a given [ratio]. + /// + /// [data] The list of [Path]s to scale. + /// [ratio] The scaling factor. + /// Returns a new list of scaled [Path]s. + /// Scales a list of [Path] objects by a given [ratio]. + /// + /// [data] The list of [Path]s to scale. + /// [ratio] The scaling factor. + /// Returns a new list of scaled [Path]s. + static List scalePaths(List data, double ratio) { + final output = []; + for (final path in data) { + output.add(scalePath(path, ratio)); + } + return output; + } + + /// Translates a list of [Path] objects by a given [location] offset. + /// + /// [data] The list of [Path]s to translate. + /// [location] The offset by which to translate the paths. + /// Returns a new list of translated [Path]s. + /// Translates a list of [Path] objects by a given [location] offset. + /// + /// [data] The list of [Path]s to translate. + /// [location] The offset by which to translate the paths. + /// Returns a new list of translated [Path]s. + static List translatePaths(List data, Offset location) { + final output = []; + final transform = Matrix4.identity(); + transform.translate(location.dx, location.dy); + for (final path in data) { + output.add(path.transform(transform.storage)); + } + return output; + } + + /// Converts a list of [CubicLine] segments into a closed [Path] representing a filled shape. + /// This is typically used for drawing thick, filled signature lines. + /// + /// [lines] The list of [CubicLine] segments. + /// [size] The base stroke width for the shape. + /// [maxSize] The maximum stroke width for the shape. + /// Returns a closed [Path] representing the filled shape. + static Path toShapePath(List lines, double size, double maxSize) { + assert(lines.isNotEmpty); + + if (lines.length == 1) { + final line = lines[0]; + if (line.isDot) { + // TODO: Consider returning null or creating a circle path directly for dots. + return Path() + ..start(line.start) + ..line(line.end); + } + return line.toShape(size, maxSize); + } + + final path = Path(); + + final firstLine = lines.first; + path.start(firstLine.start + firstLine.cpsUp(size, maxSize)); + + for (int i = 0; i < lines.length; i++) { + final line = lines[i]; + final d1 = line.cpsUp(size, maxSize); + final d2 = line.cpeUp(size, maxSize); + + path.cubic(line.cpStart + d1, line.cpEnd + d2, line.end + d2); + } + + final lastLine = lines.last; + path.line(lastLine.end + lastLine.cpeDown(size, maxSize)); + + for (int i = lines.length - 1; i > -1; i--) { + final line = lines[i]; + final d3 = line.cpeDown(size, maxSize); + final d4 = line.cpsDown(size, maxSize); + + path.cubic(line.cpEnd + d3, line.cpStart + d4, line.start + d4); + } + + path.close(); + + return path; + } + + /// Converts a list of [CubicLine] segments into a simple [Path] object, + /// connecting them with cubic Bezier curves. + /// + /// [lines] The list of [CubicLine] segments. + /// Returns a [Path] representing the connected lines. + /// Converts a list of [CubicLine] segments into a simple [Path] object, + /// connecting them with cubic Bezier curves. + /// + /// [lines] The list of [CubicLine] segments. + /// Returns a [Path] representing the connected lines. + /// Converts a list of [CubicLine] segments into a simple [Path] object, + /// connecting them with cubic Bezier curves. + /// + /// [lines] The list of [CubicLine] segments. + /// Returns a [Path] representing the connected lines. + static Path toLinePath(List lines) { + assert(lines.isNotEmpty); + + final path = Path()..start(lines[0]); + for (final line in lines) { + path.cubic(line.cpStart, line.cpEnd, line.end); + } + return path; + } +} diff --git a/hand_signature/pubspec.yaml b/hand_signature/pubspec.yaml new file mode 100644 index 0000000..eabaf92 --- /dev/null +++ b/hand_signature/pubspec.yaml @@ -0,0 +1,15 @@ +name: hand_signature +description: The Signature Pad Widget that allows you to draw smooth signatures. With variety of draw and export settings. And also supports SVG. +homepage: https://github.com/romanbase/hand_signature +version: 3.1.0+2 + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/hand_signature/test/signature_control_test.dart b/hand_signature/test/signature_control_test.dart new file mode 100644 index 0000000..147cbd1 --- /dev/null +++ b/hand_signature/test/signature_control_test.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hand_signature/signature.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + final control = HandSignatureControl(); + + // mock curve sequence + control.startPath(OffsetPoint(dx: 0.0, dy: 0.0, timestamp: 1)); + control.alterPath(OffsetPoint(dx: 10.0, dy: 10.0, timestamp: 10)); + control.alterPath(OffsetPoint(dx: 20.0, dy: 20.0, timestamp: 15)); + control.alterPath(OffsetPoint(dx: 30.0, dy: 20.0, timestamp: 20)); + control.closePath(); + + // mock dot sequence + control.startPath(OffsetPoint(dx: 30.0, dy: 30.0, timestamp: 25)); + control.closePath(); + + // json string representing above mock data + final json = + '[[{"x":0.0,"y":0.0,"t":1},{"x":10.0,"y":10.0,"t":10},{"x":20.0,"y":20.0,"t":15},{"x":30.0,"y":20.0,"t":20}],[{"x":30.0,"y":30.0,"t":25}]]'; + + group('IO', () { + test('points', () async { + final paths = control.paths; + final curve = paths[0]; + final dot = paths[1]; + + expect(paths.length, 2); + expect(curve.points.length, 4); + expect(curve.lines.length, 3); + + // velocity of first line should be lower because second line is drawn faster while distance is identical + expect(curve.lines[0].end - curve.lines[0].start, + equals(curve.lines[1].end - curve.lines[1].start)); + expect(curve.lines[0].velocity(), lessThan(curve.lines[1].velocity())); + + expect(dot.points.length, 1); + expect(dot.isDot, isTrue); + }); + + test('export', () async { + final paths = control.paths; + + final export = + '[${paths.map((e) => '[${e.points.map((e) => '{"x":${e.dx},"y":${e.dy},"t":${e.timestamp}}').join(',')}]').join(',')}]'; + final data = jsonDecode(export); + + expect(data, isNotNull); + expect((data as List).length, 2); + expect((data[0] as List).length, 4); + expect((data[1] as List).length, 1); + + expect(export, equals(json)); + }); + + test('import', () async { + final controlIn = HandSignatureControl(); + + final data = jsonDecode(json) as Iterable; + + data.forEach((element) { + final line = List.of(element); + expect(line.length, greaterThan(0)); + + //start path with first point + controlIn.startPath(OffsetPoint( + dx: line[0]['x'], + dy: line[0]['y'], + timestamp: line[0]['t'], + )); + + //skip first point and alter path with rest of points + line.skip(1).forEach((item) { + controlIn.alterPath(OffsetPoint( + dx: item['x'], + dy: item['y'], + timestamp: item['t'], + )); + }); + + //close path + controlIn.closePath(); + }); + + final paths = controlIn.paths; + final curve = paths[0]; + final dot = paths[1]; + + expect(paths.length, 2); + expect(curve.points.length, 4); + expect(curve.lines.length, 3); + + // velocity of first line is lower because second line is drawn faster while distance is identical + expect(curve.lines[0].end - curve.lines[0].start, + equals(curve.lines[1].end - curve.lines[1].start)); + expect(curve.lines[0].velocity(), lessThan(curve.lines[1].velocity())); + + expect(dot.points.length, 1); + expect(dot.isDot, isTrue); + + // check equality of individual OffsetPoints of CubePaths + expect(controlIn.equals(control), isTrue); + }); + + test('map', () async { + final controlMap = HandSignatureControl(); + controlMap.import(control.toMap()); + + // check equality of individual OffsetPoints of CubePaths + expect(controlMap.equals(control), isTrue); + }); + + test('image', () async { + final controlImage = HandSignatureControl(); + + controlImage.import(control.toMap()); + controlImage.notifyDimension(Size(1280, 720)); + + final image = await controlImage.toImage(); + + expect(image, isNotNull); + + final data = image!.buffer.asUint8List(); + + expect(data, isNotNull); + }); + }); +} diff --git a/lottie/pubspec.lock b/lottie/pubspec.lock new file mode 100644 index 0000000..e96bfd2 --- /dev/null +++ b/lottie/pubspec.lock @@ -0,0 +1,357 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analyzer: + dependency: "direct dev" + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + archive: + dependency: "direct main" + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: "direct dev" + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + pub_semver: + dependency: "direct dev" + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: "direct main" + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 + url: "https://pub.dev" + source: hosted + version: "1.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + yaml: + dependency: "direct dev" + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/mutex/pubspec.lock b/mutex/pubspec.lock index e2cf868..29340c3 100644 --- a/mutex/pubspec.lock +++ b/mutex/pubspec.lock @@ -445,10 +445,10 @@ packages: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.0" web: dependency: transitive description: diff --git a/photo_view/pubspec.lock b/photo_view/pubspec.lock index 602f2cb..8ea3764 100644 --- a/photo_view/pubspec.lock +++ b/photo_view/pubspec.lock @@ -420,10 +420,10 @@ packages: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 144765b..f8818a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,6 +5,8 @@ dependency_overrides: path: ./dependencies/dots_indicator ed25519_edwards: path: ./dependencies/ed25519_edwards + hand_signature: + path: ./dependencies/hand_signature hashlib: path: ./dependencies/hashlib hashlib_codecs: