Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/gradient color picker@mav #2

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/pixels.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
library pixels;

export 'src/editable_pixel_image.dart';
export 'src/pixel_color_picker.dart';
export 'src/pallete_color_picker.dart';
export 'src/gradient_color_picker.dart';
export 'src/pixel_editor.dart';
export 'src/pixel_image.dart';
export 'src/pixel_palette.dart';
86 changes: 55 additions & 31 deletions lib/src/editable_pixel_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,11 @@ class _EditablePixelImageState extends State<EditablePixelImage> {
return AspectRatio(
aspectRatio: widget.controller.width / widget.controller.height,
child: LayoutBuilder(builder: (context, constraints) {
final tapHandler = makeTapHandler(constraints);

return GestureDetector(
onTapDown: (details) {
var xLocal = details.localPosition.dx;
var yLocal = details.localPosition.dy;

var x = widget.controller.width * xLocal ~/ constraints.maxWidth;
var y = widget.controller.height * yLocal ~/ constraints.maxHeight;

if (widget.onTappedPixel != null) {
widget.onTappedPixel!(
PixelTapDetails._(
x: x,
y: y,
index: y * widget.controller.width + x,
localPosition: details.localPosition,
),
);
}
},
onTapDown: tapHandler,
onPanUpdate: tapHandler,
child: PixelImage(
width: widget.controller.value.width,
height: widget.controller.value.height,
Expand All @@ -73,6 +59,27 @@ class _EditablePixelImageState extends State<EditablePixelImage> {
}),
);
}

void Function(dynamic) makeTapHandler(constraints) {
return (details) {
var xLocal = details.localPosition.dx;
var yLocal = details.localPosition.dy;

var x = widget.controller.width * xLocal ~/ constraints.maxWidth;
var y = widget.controller.height * yLocal ~/ constraints.maxHeight;

if (widget.onTappedPixel != null) {
widget.onTappedPixel!(
PixelTapDetails._(
x: x,
y: y,
index: y * widget.controller.width + x,
localPosition: details.localPosition,
),
);
}
};
}
}

/// Provides details about a tapped pixel on an [EditablePixelImage].
Expand All @@ -99,13 +106,13 @@ class PixelTapDetails {

class _PixelImageValue {
final ByteData pixels;
final PixelPalette palette;
final PixelPalette? palette;
final int width;
final int height;

const _PixelImageValue({
required this.pixels,
required this.palette,
this.palette,
required this.width,
required this.height,
});
Expand All @@ -117,7 +124,7 @@ class PixelImageController extends ValueNotifier<_PixelImageValue> {
late Uint8List _pixelBytes;

/// The palette of the [EditablePixelImage] controlled by the controller.
final PixelPalette palette;
final PixelPalette? palette;

/// Height in pixels of the [EditablePixelImage] controlled by the controller.
final int height;
Expand All @@ -132,45 +139,59 @@ class PixelImageController extends ValueNotifier<_PixelImageValue> {
/// Creates a new [PixelImageController].
PixelImageController({
ByteData? pixels,
required this.palette,
this.palette,
Color? bgColor,
required this.width,
required this.height,
this.onTappedPixel,
}) : super(_PixelImageValue(
pixels: pixels ?? _emptyPixels(),
pixels: pixels ?? _emptyPixels(width, height, bgColor),
palette: palette,
width: width,
height: height,
)) {
_pixelBytes = value.pixels.buffer.asUint8List();
assert(_pixelBytes.length == width * height);
assert(_pixelBytes.length == area * 4);
}

static ByteData _emptyPixels() {
var bytes = Uint8List(64 * 64);
static ByteData _emptyPixels(int width, int height, Color? fill) {
final area = width * height;
var bytes = Uint8List(area * 4);

if (fill != null) {
for (int i = 0; i < area; i++) {
bytes[i * 4 + 0] = fill.red;
bytes[i * 4 + 1] = fill.green;
bytes[i * 4 + 2] = fill.blue;
bytes[i * 4 + 3] = fill.alpha;
}
}
return bytes.buffer.asByteData();
}

/// Gets or sets the [ByteData] of the [EditablePixelImage] controlled by the
/// controller.
ByteData get pixels => _pixelBytes.buffer.asByteData();

/// calculate the image's 2D area
int get area => width * height;

set pixels(ByteData pixels) {
assert(pixels.lengthInBytes == width * height);
assert(pixels.lengthInBytes == area * 4);
_pixelBytes = pixels.buffer.asUint8List();
_update();
}

/// Sets a specific pixel in the [EditablePixelImage] controlled by the
/// controller.
void setPixel({
required int colorIndex,
required Color color,
required int x,
required int y,
}) {
setPixelIndex(
pixelIndex: y * width + x,
colorIndex: colorIndex,
color: color,
);
_update();
}
Expand All @@ -179,9 +200,12 @@ class PixelImageController extends ValueNotifier<_PixelImageValue> {
/// controller.
void setPixelIndex({
required pixelIndex,
required colorIndex,
required color,
}) {
_pixelBytes[pixelIndex] = colorIndex;
_pixelBytes[pixelIndex * 4 + 0] = color.red;
_pixelBytes[pixelIndex * 4 + 1] = color.green;
_pixelBytes[pixelIndex * 4 + 2] = color.blue;
_pixelBytes[pixelIndex * 4 + 3] = color.alpha;
_update();
}

Expand Down
201 changes: 201 additions & 0 deletions lib/src/gradient_color_picker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import 'dart:math';

import 'package:flutter/material.dart';

/// A gradient-equation based color picker. It can be displayed vertically or
/// horizontally depending on the [direction].
class GradientColorPicker extends StatelessWidget {
/// Defines if the color picker should be displayed horizontally or
/// vertically.
final Axis direction;

/// A callback for when the user picks a color.
final void Function(Color color) onSelected;

/// A callback to calculate the color
final Color Function(double y) equation;

/// The width or height (depending on it's direction) of the color picker.
final double crossAxisWidth;

/// Dictates the resolution of the visualized gradient. More = smoother.
final int bands = 255;

/// Changes the slider gizmo color
final Color sliderColor;

/// Sets the gizmo starting position
final double sliderStartOffset;

/// Creates a new [GradientColorPicker].
const GradientColorPicker({
required this.equation,
required this.onSelected,
this.direction = Axis.horizontal,
this.crossAxisWidth = 32.0,
this.sliderColor = Colors.white,
this.sliderStartOffset = 0.5,
super.key,
});

@override
Widget build(BuildContext context) {
return SizedBox(
width: direction == Axis.vertical ? crossAxisWidth : null,
height: direction == Axis.horizontal ? crossAxisWidth : null,
child: Flex(
direction: direction,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: _GradientColorPickerWell(
equation: equation,
onTap: (x, y) =>
onSelected(equation(direction == Axis.horizontal ? x : y)),
bands: bands,
sliderColor: sliderColor,
sliderStartOffset: sliderStartOffset,
direction: direction),
)
],
),
);
}
}

class _GradientColorPickerWell extends StatefulWidget {
final Function(double x, double y) onTap;
final GlobalKey colorGradKey = GlobalKey(debugLabel: "colorGradient");
final Color Function(double y) equation;
final int bands;
final Color sliderColor;
final double sliderStartOffset;
final Axis direction;

_GradientColorPickerWell(
{required this.equation,
required this.onTap,
required this.bands,
required this.sliderColor,
required this.sliderStartOffset,
required this.direction});

Point pointFromTapDetails(details) {
final RenderBox renderBox =
colorGradKey.currentContext?.findRenderObject() as RenderBox;
final size = renderBox.size;
final double x = details.localPosition.dx / size.width;
final double y = details.localPosition.dy / size.height;
return Point(x, y);
}

@override
State<_GradientColorPickerWell> createState() {
return _GradientColorPickerWellState();
}
}

class _GradientColorPickerWellState extends State<_GradientColorPickerWell> {
late double offset;

@override
void initState() {
offset = widget.sliderStartOffset;
super.initState();
}

@override
Widget build(BuildContext context) {
final List<Color> colors = [];
final List<double> stops = List.filled(widget.bands, 0.0);

for (int i = 0; i < widget.bands; i++) {
double alpha = i / widget.bands;

if (widget.direction == Axis.horizontal) {
alpha = 1.0 - alpha;
}

colors.add(widget.equation(alpha));
stops[i] = i / widget.bands;
}

return GestureDetector(
onTapDown: (details) {
final p = widget.pointFromTapDetails(details);
final double x = p.x.toDouble();
final double y = p.y.toDouble();
widget.onTap(x, y);
setState(() {
offset = widget.direction == Axis.horizontal ? x : y;
});
},
child: Stack(children: [
makeSliderGizmo(),
Container(
key: widget.colorGradKey,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
stops: stops,
colors: colors,
)),
),
makeSliderGizmo(),
]));
}

Widget makeSliderGizmo() => CustomPaint(
size: Size.infinite,
painter: GradientSliderPainter(
offset, widget.direction, widget.bands, widget.sliderColor),
willChange: true,
);
}

/// Custom paints the slider gizmo
class GradientSliderPainter extends CustomPainter {
/// The offset along the align axis
double offset;

/// The align axis
Axis direction;

/// Helps calculate where the slider should snap to
int bands;

/// The stroke color of the slider
Color borderColor;

/// Use the axis [offset] to position the gizmo
GradientSliderPainter(
this.offset, this.direction, this.bands, this.borderColor)
: super();

/// Paints a white rectangle representing the slider gizmo
@override
void paint(Canvas canvas, Size size) {
Offset center = Offset(size.width / 2.0, offset * size.height);
double width = size.width;
double height = size.height / bands;

if (direction == Axis.horizontal) {
center = Offset(offset * size.width, size.height / 2.0);
width = size.width / bands;
height = size.height;
}

canvas.drawRect(
Rect.fromCenter(center: center, width: width, height: height),
Paint()
..color = borderColor
..strokeWidth = 2.0
..style = PaintingStyle.stroke);
}

/// Never repaint unless changed
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Loading