Created
March 28, 2021 14:19
-
-
Save jogboms/7f5aa471859a2237f80b83aefeb62da7 to your computer and use it in GitHub Desktop.
Studio Pro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async' as async; | |
import 'dart:math' as math; | |
import 'dart:ui' as ui; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/gestures.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
void main() => runApp( | |
MaterialApp( | |
theme: ThemeData.dark(), | |
debugShowCheckedModeBanner: false, | |
home: Playground(), | |
), | |
); | |
class Playground extends StatefulWidget { | |
@override | |
_PlaygroundState createState() => _PlaygroundState(); | |
} | |
class _PlaygroundState extends State<Playground> with TickerProviderStateMixin { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
backgroundColor: Color(0xFF1D232F), | |
body: Center( | |
child: Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 100.0), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
SizedBox.fromSize( | |
size: Size(125, 600), | |
child: LevelSliderWidget(value: 5, min: -10, max: 25), | |
), | |
SizedBox(width: 16), | |
SizedBox.fromSize( | |
size: Size.square(600), | |
child: KnobControlWidget(value: 25, min: 0, max: 70), | |
), | |
SizedBox(width: 32), | |
SizedBox.fromSize( | |
size: Size(125, 600), | |
child: LevelWidget(value: -14, min: -60, max: 0), | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
} | |
class LevelWidget extends LeafRenderObjectWidget { | |
const LevelWidget({Key? key, required this.value, required this.min, required this.max, this.step = 10}) | |
: assert(value <= max && value >= min), | |
super(key: key); | |
final double value; | |
final double max; | |
final double min; | |
final int step; | |
@override | |
RenderObject createRenderObject(BuildContext context) { | |
return RenderLevelWidget(value: value, min: min, max: max, step: step); | |
} | |
@override | |
void updateRenderObject(BuildContext context, covariant RenderLevelWidget renderObject) { | |
renderObject | |
..value = value | |
..min = min | |
..max = max | |
..step = step; | |
} | |
} | |
class RenderLevelWidget extends RenderBox with RenderBoxDebugBounds { | |
RenderLevelWidget({required double value, required double min, required double max, required int step}) | |
: _value = valueToPercentageBuilder(min, max)(value), | |
_min = min, | |
_max = max, | |
_step = step; | |
double _value; | |
set value(double value) { | |
final percentage = valueToPercentageBuilder(_min, _max)(value); | |
if (_value == percentage) { | |
return; | |
} | |
_value = percentage; | |
markNeedsPaint(); | |
} | |
double _max; | |
set max(double value) { | |
if (_max == value) { | |
return; | |
} | |
_max = value; | |
_value = valueToPercentageBuilder(_min, _max)(_value); | |
markNeedsPaint(); | |
} | |
double _min; | |
set min(double value) { | |
if (_min == value) { | |
return; | |
} | |
_min = value; | |
_value = valueToPercentageBuilder(_min, _max)(_value); | |
markNeedsPaint(); | |
} | |
int _step; | |
set step(int value) { | |
if (_step == value) { | |
return; | |
} | |
_step = value; | |
markNeedsPaint(); | |
} | |
late async.Timer _timer; | |
@override | |
void attach(PipelineOwner owner) { | |
super.attach(owner); | |
_timer = async.Timer.periodic(Duration(milliseconds: 1000 ~/ 24), (timer) { | |
_value = random(0, .25); | |
markNeedsPaint(); | |
}); | |
async.Timer(Duration(seconds: 5), _timer.cancel); | |
} | |
@override | |
void detach() { | |
_timer.cancel(); | |
super.detach(); | |
} | |
static final selectedColor = Color(0xFFFF6E40); | |
static final thumbColor = Color(0xFF29323F); | |
static final tickColor = Color(0xFF374153); | |
static final labelColor = Color(0xFF69707C); | |
static final backgroundColor = Color(0xFF151C24); | |
static const tickDivisions = 10.0; | |
int get tickCount => ((_max - _min) ~/ _step) + 1; | |
double get tickLineCount => (tickCount - 1) * tickDivisions; | |
static double Function(double) percentageToValueBuilder(double min, double max, int tickCount) { | |
return interpolate(inputMax: tickCount - 1, outputMax: max, outputMin: min); | |
} | |
static double Function(double) valueToPercentageBuilder(double min, double max) { | |
return interpolate(inputMax: max, inputMin: min, outputMin: 1, outputMax: 0); | |
} | |
@override | |
bool get sizedByParent => true; | |
@override | |
bool get isRepaintBoundary => true; | |
@override | |
Size computeDryLayout(BoxConstraints constraints) { | |
return constraints.biggest; | |
} | |
@override | |
void paint(PaintingContext context, Offset offset) { | |
debugBounds.add(offset & size); | |
final canvas = context.canvas; | |
final width = size.width; | |
final trackWidth = width * .55; | |
final labelColumnWidth = width - trackWidth; | |
final height = size.height * .925; | |
final bounds = Rect.fromCenter(center: size.center(offset), width: size.width, height: height); | |
debugBounds.add(bounds); | |
final selectedHeight = _value * height; | |
final selectedOffset = bounds.topLeft.translate(0, selectedHeight); | |
final selectedCenterOffset = selectedOffset.translate(trackWidth / 2, 0); | |
final tickThickness = trackWidth * .025; | |
final tickSpacing = height / tickLineCount; | |
final labelFontSize = labelColumnWidth * .3; | |
final labelTopRightOffset = bounds.topRight + Offset(labelColumnWidth * -.5, 0); | |
for (var i = 0; i < tickLineCount + 1; i++) { | |
final isOnBorders = i == 0 || i == tickLineCount; | |
final verticalPosition = i * tickSpacing; | |
final verticalOffset = Offset(0, verticalPosition); | |
final startOffset = bounds.topLeft + verticalOffset; | |
final endOffset = startOffset + Offset(trackWidth, 0); | |
final inBetween = | |
verticalPosition >= selectedHeight || (selectedHeight - verticalPosition <= precisionErrorTolerance); | |
canvas.drawLine( | |
startOffset, | |
endOffset, | |
Paint() | |
..color = inBetween ? selectedColor : tickColor | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.round | |
..strokeWidth = isOnBorders ? tickThickness * 1 : tickThickness, | |
); | |
if (i % tickDivisions != 0) { | |
continue; | |
} | |
final labelBounds = canvas.drawText( | |
'${percentageToValueBuilder(_min, _max, tickCount)(tickCount - 1 - (i / tickDivisions)).toInt()}', | |
center: labelTopRightOffset + verticalOffset, | |
style: TextStyle(color: labelColor, fontSize: labelFontSize), | |
); | |
debugBounds.add(labelBounds); | |
} | |
final trackStrokeWidth = trackWidth / 10; | |
final trackBounds = bounds.topLeft & Size(trackWidth, height); | |
canvas.drawLine( | |
trackBounds.topCenter, | |
trackBounds.bottomCenter, | |
Paint() | |
..color = backgroundColor | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.square | |
..strokeWidth = trackStrokeWidth, | |
); | |
debugBounds.add(trackBounds); | |
canvas.drawLine( | |
selectedCenterOffset.translate(0, -tickThickness / 2), | |
trackBounds.bottomCenter.translate(0, tickThickness / 2), | |
Paint() | |
..color = selectedColor | |
..style = PaintingStyle.stroke | |
..strokeWidth = trackStrokeWidth, | |
); | |
} | |
} | |
class LevelSliderWidget extends LeafRenderObjectWidget { | |
const LevelSliderWidget({Key? key, required this.value, required this.min, required this.max, this.step = 5}) | |
: assert(value <= max && value >= min), | |
super(key: key); | |
final double value; | |
final double max; | |
final double min; | |
final int step; | |
@override | |
RenderObject createRenderObject(BuildContext context) { | |
return RenderLevelSlider(value: value, min: min, max: max, step: step); | |
} | |
@override | |
void updateRenderObject(BuildContext context, covariant RenderLevelSlider renderObject) { | |
renderObject | |
..value = value | |
..min = min | |
..max = max | |
..step = step; | |
} | |
} | |
class RenderLevelSlider extends RenderBox with RenderBoxDebugBounds { | |
RenderLevelSlider({required double value, required double min, required double max, required int step}) | |
: _value = valueToPercentageBuilder(min, max)(value), | |
_min = min, | |
_max = max, | |
_step = step { | |
drag = VerticalDragGestureRecognizer() | |
..onStart = _onDragStart | |
..onUpdate = _onDragUpdate; | |
tap = TapGestureRecognizer()..onTapDown = _onTapDown; | |
} | |
double _value = 0.0; | |
set value(double value) { | |
final percentage = valueToPercentageBuilder(_min, _max)(value); | |
if (_value == percentage) { | |
return; | |
} | |
_value = percentage; | |
markNeedsPaint(); | |
} | |
double _max; | |
set max(double value) { | |
if (_max == value) { | |
return; | |
} | |
_max = value; | |
_value = valueToPercentageBuilder(_min, _max)(_value); | |
markNeedsPaint(); | |
} | |
double _min; | |
set min(double value) { | |
if (_min == value) { | |
return; | |
} | |
_min = value; | |
_value = valueToPercentageBuilder(_min, _max)(_value); | |
markNeedsPaint(); | |
} | |
int _step; | |
set step(int value) { | |
if (_step == value) { | |
return; | |
} | |
_step = value; | |
markNeedsPaint(); | |
} | |
late final VerticalDragGestureRecognizer drag; | |
late final TapGestureRecognizer tap; | |
static final selectedColor = Color(0xFFFF6E40); | |
static final thumbColor = Color(0xFF29323F); | |
static final tickColor = Color(0xFF374153); | |
static final labelColor = Color(0xFF69707C); | |
static final backgroundColor = Color(0xFF151C24); | |
static final thumbGradient = [tickColor, thumbColor]; | |
static const tickDivisions = 10.0; | |
int get tickCount => ((_max - _min) ~/ _step) + 1; | |
double get tickLineCount => (tickCount - 1) * tickDivisions; | |
late var offsetToPercentageValueBuilder = interpolate(inputMax: trackBounds.height); | |
static double Function(double) percentageToValueBuilder(double min, double max, int tickCount) { | |
return interpolate(inputMax: tickCount - 1, outputMax: max, outputMin: min); | |
} | |
static double Function(double) valueToPercentageBuilder(double min, double max) { | |
return interpolate(inputMax: max, inputMin: min, outputMin: 1, outputMax: 0); | |
} | |
late double thumbHeight; | |
late Rect thumbBounds; | |
late Rect trackBounds; | |
double _verticalDragOffset = 0.0; | |
void _onTapDown(TapDownDetails details) { | |
_verticalDragOffset = details.localPosition.dy; | |
_onChangeOffset(); | |
} | |
void _onDragStart(DragStartDetails details) { | |
_verticalDragOffset = details.localPosition.dy; | |
} | |
void _onDragUpdate(DragUpdateDetails details) { | |
_verticalDragOffset += details.primaryDelta ?? 0.0; | |
_onChangeOffset(); | |
} | |
void _onChangeOffset() { | |
_value = offsetToPercentageValueBuilder( | |
(_verticalDragOffset - (thumbBounds.height / 2)).clamp(0.0, trackBounds.height), | |
); | |
markNeedsPaint(); | |
} | |
@override | |
bool get sizedByParent => true; | |
@override | |
bool get isRepaintBoundary => true; | |
@override | |
Size computeDryLayout(BoxConstraints constraints) { | |
return constraints.biggest; | |
} | |
@override | |
bool hitTestSelf(Offset position) { | |
return thumbBounds.contains(position) || trackBounds.contains(position); | |
} | |
@override | |
void handleEvent(PointerEvent event, HitTestEntry entry) { | |
debugHandleEvent(event, entry); | |
if (event is PointerDownEvent) { | |
if (thumbBounds.contains(event.localPosition)) { | |
drag.addPointer(event); | |
} else { | |
tap.addPointer(event); | |
} | |
} | |
} | |
@override | |
void paint(PaintingContext context, Offset offset) { | |
debugBounds.add(offset & size); | |
final canvas = context.canvas; | |
final trackWidth = size.width * .55; | |
final labelColumnWidth = size.width - trackWidth; | |
thumbHeight = size.height * .1; | |
final height = size.height - thumbHeight; | |
final bounds = Rect.fromCenter(center: size.center(offset), width: size.width, height: height); | |
debugBounds.add(bounds); | |
trackBounds = bounds.topLeft & Size(trackWidth, height); | |
debugBounds.add(trackBounds); | |
thumbBounds = Rect.fromCenter( | |
center: Offset(trackBounds.center.dx, bounds.top + _value * height), | |
width: trackWidth * 1.25, | |
height: thumbHeight, | |
); | |
debugBounds.add(thumbBounds); | |
final tickThickness = trackWidth * .025; | |
final tickSpacing = height / tickLineCount; | |
final labelFontSize = labelColumnWidth * .3; | |
final labelTopRightOffset = bounds.topRight + Offset(labelColumnWidth * -.5, 0); | |
for (var i = 0; i < tickLineCount + 1; i++) { | |
final isOnBorders = i == 0 || i == tickLineCount; | |
final verticalPosition = i * tickSpacing; | |
final verticalOffset = Offset(0, verticalPosition); | |
final startOffset = bounds.topLeft + verticalOffset; | |
final endOffset = startOffset + Offset(trackWidth, 0); | |
final inBetween = verticalPosition.between(thumbBounds.center.dy, height); | |
canvas.drawLine( | |
startOffset, | |
endOffset, | |
Paint() | |
..color = inBetween ? selectedColor : tickColor | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.round | |
..strokeWidth = isOnBorders ? tickThickness * 2 : tickThickness, | |
); | |
if (i % tickDivisions != 0) { | |
continue; | |
} | |
final labelBounds = canvas.drawText( | |
'${percentageToValueBuilder(_min, _max, tickCount)(tickCount - 1 - (i / tickDivisions)).toInt()}', | |
center: labelTopRightOffset + verticalOffset, | |
style: TextStyle( | |
color: thumbBounds.contains(startOffset) ? selectedColor : labelColor, | |
fontSize: labelFontSize, | |
), | |
); | |
debugBounds.add(labelBounds); | |
} | |
final trackStrokeWidth = trackWidth * .3; | |
canvas.drawLine( | |
trackBounds.topCenter, | |
trackBounds.bottomCenter, | |
Paint() | |
..color = backgroundColor | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.round | |
..strokeWidth = trackStrokeWidth, | |
); | |
canvas.drawLine( | |
thumbBounds.center, | |
trackBounds.bottomCenter, | |
Paint() | |
..color = selectedColor | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.round | |
..strokeWidth = trackStrokeWidth, | |
); | |
canvas.drawRect( | |
thumbBounds.translate(0, thumbBounds.radius * .65), | |
Paint() | |
..maskFilter = MaskFilter.blur(BlurStyle.normal, thumbBounds.radius * .5) | |
..color = Colors.black87, | |
); | |
canvas.drawRRect( | |
RRect.fromRectAndRadius(thumbBounds, Radius.circular(tickSpacing)), | |
Paint() | |
..shader = LinearGradient( | |
begin: Alignment.topCenter, | |
end: Alignment.bottomCenter, | |
colors: thumbGradient, | |
).createShader(thumbBounds), | |
); | |
canvas.drawLine( | |
thumbBounds.centerLeft, | |
thumbBounds.centerRight, | |
Paint() | |
..color = selectedColor | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.round | |
..strokeWidth = tickThickness * 2, | |
); | |
} | |
} | |
class KnobControlWidget extends LeafRenderObjectWidget { | |
const KnobControlWidget({Key? key, required this.value, required this.min, required this.max}) : super(key: key); | |
final double value; | |
final double max; | |
final double min; | |
@override | |
RenderObject createRenderObject(BuildContext context) { | |
return RenderKnobControl(value: value, min: min, max: max); | |
} | |
@override | |
void updateRenderObject(BuildContext context, covariant RenderKnobControl renderObject) { | |
renderObject | |
..value = value | |
..min = min | |
..max = max; | |
} | |
} | |
class RenderKnobControl extends RenderBox with RenderBoxDebugBounds { | |
RenderKnobControl({required double value, required double min, required double max}) | |
: _value = valueToAngleBuilder(min, max)(value), | |
_min = min, | |
_max = max { | |
drag = PanGestureRecognizer() | |
..onStart = _onDragStart | |
..onUpdate = _onDragUpdate | |
..onCancel = _onDragCancel | |
..onEnd = (_) => _onDragCancel(); | |
} | |
double _value; | |
set value(double value) { | |
final angle = valueToAngleBuilder(_min, _max)(value); | |
if (_value == angle) { | |
return; | |
} | |
_value = angle; | |
markNeedsPaint(); | |
} | |
double _max; | |
set max(double value) { | |
if (_max == value) { | |
return; | |
} | |
_max = value; | |
_value = valueToAngleBuilder(_min, _max)(_value); | |
markNeedsPaint(); | |
} | |
double _min; | |
set min(double value) { | |
if (_min == value) { | |
return; | |
} | |
_min = value; | |
_value = valueToAngleBuilder(_min, _max)(_value); | |
markNeedsPaint(); | |
} | |
late DragGestureRecognizer drag; | |
static final totalAngle = 360.radians; | |
static final startAngle = -90.radians; | |
static final selectedColor = Color(0xFFFF6E40); | |
static final tickColor = Color(0xFF374153); | |
static final thumbColor = Color(0xFF29323F); | |
static final backgroundColor = Color(0xFF151C24); | |
static final thumbGradient = [tickColor, thumbColor]; | |
static const tickDivisions = 4.0; | |
static double Function(double) angleToValueBuilder(double min, double max) { | |
return interpolate(inputMax: totalAngle, outputMin: min, outputMax: max); | |
} | |
static double Function(double) valueToAngleBuilder(double min, double max) { | |
return (value) => interpolate(inputMin: min, inputMax: max)(value) * totalAngle; | |
} | |
late Rect knobBounds; | |
Offset _currentDragOffset = Offset.zero; | |
void _onDragStart(DragStartDetails details) { | |
_currentDragOffset = details.localPosition; | |
} | |
void _onDragUpdate(DragUpdateDetails details) { | |
final previousOffset = _currentDragOffset; | |
_currentDragOffset += details.delta; | |
final diffInAngle = toAngle(_currentDragOffset, knobBounds.center) - toAngle(previousOffset, knobBounds.center); | |
final angle = (_value + diffInAngle).normalizeAngle; | |
if (angle == _value) { | |
return; | |
} | |
_value = angle; | |
markNeedsPaint(); | |
} | |
void _onDragCancel() { | |
_currentDragOffset = Offset.zero; | |
} | |
@override | |
bool hitTestSelf(Offset position) { | |
return knobBounds.contains(position); | |
} | |
@override | |
void handleEvent(PointerEvent event, HitTestEntry entry) { | |
if (event is PointerDownEvent) { | |
drag.addPointer(event); | |
} | |
} | |
@override | |
bool get sizedByParent => true; | |
@override | |
bool get isRepaintBoundary => true; | |
@override | |
Size computeDryLayout(BoxConstraints constraints) { | |
return constraints.biggest; | |
} | |
@override | |
void paint(PaintingContext context, Offset offset) { | |
final canvas = context.canvas; | |
final bounds = offset & size; | |
final radius = bounds.radius; | |
final center = bounds.center; | |
debugBounds.add(bounds); | |
canvas.drawCircle(center, radius, Paint()..color = backgroundColor); | |
final sweepAngle = _value; | |
final endAngle = startAngle + sweepAngle; | |
final outerPadding = radius * .045; | |
final trackRadius = radius - outerPadding; | |
final knobRadius = trackRadius * .8; | |
final trackHeight = trackRadius - knobRadius; | |
final tickHeight = trackHeight / 2; | |
final tickStrokeWidth = trackHeight * .075; | |
for (var i = 0; i < totalAngle.degrees / tickDivisions; i++) { | |
final angle = startAngle + (i * tickDivisions).radians; | |
final isLongTick = startAngle == angle || (angle - endAngle).abs() < 0.01; | |
final heightOffset = isLongTick ? 0 : tickHeight * .3; | |
final startOffset = center + Offset.fromDirection(angle, trackRadius); | |
final endOffset = startOffset + Offset.fromDirection(angle, -tickHeight + heightOffset); | |
final inBetween = angle.between(startAngle, endAngle); | |
canvas.drawLine( | |
startOffset, | |
endOffset, | |
Paint() | |
..color = inBetween ? selectedColor : tickColor | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.round | |
..strokeWidth = inBetween ? tickStrokeWidth : tickStrokeWidth * .75, | |
); | |
} | |
final trackRingStrokeWidth = trackHeight * .125; | |
final trackRingRadius = trackRadius - (trackHeight / 2); | |
canvas.drawArc( | |
Rect.fromCircle(center: center, radius: trackRingRadius - (trackRingStrokeWidth / 2)), | |
startAngle, | |
totalAngle, | |
false, | |
Paint() | |
..color = tickColor | |
..style = PaintingStyle.stroke | |
..strokeWidth = trackRingStrokeWidth, | |
); | |
final selectedArcPath = Path() | |
..moveTo(center.dx, center.dy) | |
..addArc(Rect.fromCircle(center: center, radius: trackRingRadius), startAngle, sweepAngle) | |
..lineTo(center.dx, center.dy) | |
..close(); | |
debugPaths.add(selectedArcPath); | |
canvas.drawPath(selectedArcPath, Paint()..color = selectedColor); | |
canvas.drawPath( | |
selectedArcPath, | |
Paint() | |
..color = selectedColor | |
..style = PaintingStyle.stroke | |
..strokeWidth = tickStrokeWidth, | |
); | |
knobBounds = Rect.fromCircle(center: center, radius: knobRadius); | |
canvas.drawCircle( | |
center + Offset.fromDirection(endAngle, knobRadius * .275), | |
knobRadius, | |
Paint() | |
..maskFilter = MaskFilter.blur(BlurStyle.normal, knobRadius * .25) | |
..color = Colors.black, | |
); | |
canvas.drawCircle( | |
center, | |
knobRadius, | |
Paint() | |
..shader = LinearGradient( | |
begin: Alignment.topCenter, | |
end: Alignment.bottomCenter, | |
colors: thumbGradient, | |
transform: GradientRotation(endAngle - 90.radians), | |
).createShader(knobBounds), | |
); | |
final indicatorCenter = center + Offset.fromDirection(endAngle, knobRadius * .85); | |
canvas.drawCircle(indicatorCenter, knobRadius * .0375, Paint()..color = selectedColor); | |
canvas.drawCircle( | |
indicatorCenter, | |
knobRadius * .05, | |
Paint() | |
..color = backgroundColor.withOpacity(.5) | |
..style = PaintingStyle.stroke | |
..strokeWidth = knobRadius * .015, | |
); | |
final value = angleToValueBuilder(_min, _max)(sweepAngle); | |
final valueTextSize = knobRadius * .75; | |
final captionTextSize = knobRadius * .1; | |
final valueTextRect = canvas.drawText( | |
'${value.toInt()}', | |
center: center.translate(0, -captionTextSize / 2), | |
style: TextStyle(color: Colors.white, fontSize: valueTextSize), | |
); | |
debugBounds.add(valueTextRect); | |
final captionTextRect = canvas.drawText( | |
'Processing', | |
center: center.translate(0, valueTextSize / 2), | |
style: TextStyle( | |
color: selectedColor.withOpacity(.85), | |
fontSize: captionTextSize, | |
fontWeight: FontWeight.w500, | |
letterSpacing: 1, | |
), | |
); | |
debugBounds.add(captionTextRect); | |
} | |
} | |
extension NumX<T extends num> on T { | |
static double twoPi = math.pi * 2.0; | |
double get degrees => (this * 180.0) / math.pi; | |
double get radians => (this * math.pi) / 180.0; | |
T normalize(T max) => (this % max + max) % max as T; | |
double get normalizeAngle => normalize(twoPi as T).toDouble(); | |
bool between(double min, double max) { | |
return this <= max && this >= min; | |
} | |
} | |
extension RectX on ui.Rect { | |
double get radius => shortestSide / 2; | |
} | |
extension CanvasX on Canvas { | |
Rect drawText( | |
String text, { | |
required Offset center, | |
TextStyle style = const TextStyle(fontSize: 14.0, color: Color(0xFF333333), fontWeight: FontWeight.normal), | |
}) { | |
final textPainter = TextPainter(textAlign: TextAlign.center, textDirection: TextDirection.ltr) | |
..text = TextSpan(text: text, style: style) | |
..layout(); | |
final bounds = (center & textPainter.size).translate(-textPainter.width / 2, -textPainter.height / 2); | |
textPainter.paint(this, bounds.topLeft); | |
return bounds; | |
} | |
} | |
double toAngle(ui.Offset position, ui.Offset center) { | |
return (position - center).direction; | |
} | |
double random(double min, double max) { | |
return math.max(math.min(max, math.Random().nextDouble() * max), min); | |
} | |
mixin RenderBoxDebugBounds on RenderBox { | |
Set<Rect> debugBounds = {}; | |
Set<Path> debugPaths = {}; | |
@override | |
void debugPaint(PaintingContext context, ui.Offset offset) { | |
assert(() { | |
super.debugPaint(context, offset); | |
if (debugPaintSizeEnabled) { | |
debugBounds.forEach((bounds) { | |
context.canvas.drawRect( | |
bounds, | |
Paint() | |
..style = PaintingStyle.stroke | |
..color = const Color(0xFF00FFFF)); | |
}); | |
debugPaths.forEach((path) { | |
context.canvas.drawPath( | |
path, | |
Paint() | |
..style = PaintingStyle.stroke | |
..color = const Color(0xFF00FFFF)); | |
}); | |
} | |
return true; | |
}()); | |
debugBounds.clear(); | |
debugPaths.clear(); | |
} | |
} | |
// https://stackoverflow.com/a/55088673/8236404 | |
double Function(double input) interpolate({ | |
double? inputMin = 0, | |
double? inputMax = 1, | |
double? outputMin = 0, | |
double? outputMax = 1, | |
}) { | |
// null check | |
if (inputMin == null || inputMax == null || outputMin == null || outputMax == null) { | |
throw Exception("You can't have nulls as parameters"); | |
} | |
//range check | |
if (inputMin == inputMax) { | |
print('Warning: Zero input range'); | |
return (x) => inputMax; | |
} | |
if (outputMin == outputMax) { | |
print('Warning: Zero output range'); | |
return (x) => outputMax; | |
} | |
//check reversed input range | |
var reverseInput = false; | |
final oldMin = math.min(inputMin, inputMax); | |
final oldMax = math.max(inputMin, inputMax); | |
if (oldMin != inputMin) { | |
reverseInput = true; | |
} | |
//check reversed output range | |
var reverseOutput = false; | |
final newMin = math.min(outputMin, outputMax); | |
final newMax = math.max(outputMin, outputMax); | |
if (newMin != outputMin) { | |
reverseOutput = true; | |
} | |
// Hot-rod the most common case. | |
if (!reverseInput && !reverseOutput) { | |
final dNew = newMax - newMin; | |
final dOld = oldMax - oldMin; | |
return (x) { | |
return ((x - oldMin) * dNew / dOld) + newMin; | |
}; | |
} | |
return (x) { | |
double portion; | |
if (reverseInput) { | |
portion = (oldMax - x) * (newMax - newMin) / (oldMax - oldMin); | |
} else { | |
portion = (x - oldMin) * (newMax - newMin) / (oldMax - oldMin); | |
} | |
double result; | |
if (reverseOutput) { | |
result = newMax - portion; | |
} else { | |
result = portion + newMin; | |
} | |
return result; | |
}; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment