Skip to content

Instantly share code, notes, and snippets.

@roipeker
Last active August 29, 2021 12:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save roipeker/6e7d5b30f6b022196bc98e2db14676a2 to your computer and use it in GitHub Desktop.
Save roipeker/6e7d5b30f6b022196bc98e2db14676a2 to your computer and use it in GitHub Desktop.
GraphX Color Picker (based on SuperDeclarative video).
/// roipeker 2020
///
/// GraphX code picker sample, inspired by SuperDeclarative video:
/// https://www.youtube.com/watch?v=HURA4DKjA1c
///
/// web demo:
/// https://roi-graphx-color-picker.surge.sh
///
/// GraphX package: https://pub.dev/packages/graphx
///
/// NOTE: No code cleanup, nor refactoring was made.
///
import 'package:flutter/material.dart';
import 'package:graphx/graphx.dart';
import 'js_utils.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
title: 'GraphX color picker.',
home: Scaffold(
appBar: AppBar(
title: Row(
children: [
Text('graphx color picker'),
Spacer(),
Column(
children: [
Text(
'based on SuperDeclarative! video.',
style: TextStyle(fontSize: 10, color: Colors.white54),
),
],
),
],
),
),
body: Center(
child: PickerContainer(),
),
bottomNavigationBar: Container(
height: 50,
color: Colors.black87,
padding: EdgeInsets.all(10),
child: Row(
children: [
SizedBox(width: 12),
TextButton(
child: Text(
'graphx gist',
style: TextStyle(fontSize: 12),
),
onPressed: () => openUrl(
'https://gist.github.com/roipeker/6e7d5b30f6b022196bc98e2db14676a2'),
),
VerticalDivider(),
TextButton(
child: Text(
'original workshop',
style: TextStyle(fontSize: 12),
),
onPressed: () =>
openUrl('https://www.youtube.com/watch?v=HURA4DKjA1c'),
),
],
),
),
));
}
}
class PickerContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
const separator = SizedBox(width: 10);
return Container(
width: 450,
height: 300,
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(20.0),
boxShadow: [
BoxShadow(
color: Colors.black45,
blurRadius: 24,
offset: Offset(0, 2),
),
],
),
padding: EdgeInsets.all(12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ValueListenableBuilder<Color>(
valueListenable: pickerNotifier,
builder: (ctx, value, _) => Container(
width: 40,
decoration: BoxDecoration(
color: value,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
),
),
),
separator,
Expanded(
child: Container(
child: SceneBuilderWidget(
builder: () => SceneController.withLayers(front: ValueScene()),
),
),
),
separator,
Container(
width: 80,
child: SceneBuilderWidget(
builder: () => SceneController.withLayers(front: HueScene()),
),
),
],
),
);
}
}
class ValueScene extends SceneRoot {
Shape bgColor;
Shape bgBrightness;
Shape bgValue;
Sprite colorsContainer;
ByteData colorsBytes;
Shape selector;
Color _selectedColor;
double sw, sh;
ValueScene() {
config(usePointer: true, autoUpdateAndRender: true);
}
@override
void addedToStage() {
sw = stage.stageWidth;
sh = stage.stageHeight;
bgColor = Shape();
bgColor.graphics.beginFill(0x0000ff).drawRect(0, 0, sw, sh).endFill();
bgBrightness = Shape();
drawGradient(bgBrightness.graphics, color: 0xffffff, isHorizontal: true);
bgValue = Shape();
drawGradient(bgValue.graphics, color: 0x0, isHorizontal: false);
final radius = 8.0;
selector = Shape();
selector.graphics
.lineStyle(2, 0xffffff)
.drawCircle(0, 0, radius)
.endFill()
.lineStyle(2, 0x0)
.drawCircle(0, 0, radius - 2)
.endFill();
selector.alpha = .8;
colorsContainer = Sprite();
colorsContainer.addChild(bgColor);
colorsContainer.addChild(bgBrightness);
colorsContainer.addChild(bgValue);
addChild(colorsContainer);
addChild(selector);
mouseChildren = false;
stage.onMouseDown.add((e) {
_updateColor();
selector.tween(duration: .4, scale: 1.5);
GMouse.hide();
stage.onMouseUp.addOnce((e) {
GMouse.show();
selector.tween(duration: .3, scale: 1);
});
});
stage.onMouseMove.add(_handleMouseMove);
pickerMPS.on(ColorPickerEmitter.changeHue, handleChangeHue);
}
void _handleMouseMove(MouseInputData input) {
if (input.isPrimaryDown) {
_updateColor();
}
}
void _updateColor() {
selector.x = mouseX.clamp(0.0, sw - 1);
selector.y = mouseY.clamp(0.0, sh - 1);
updateColor();
}
void drawGradient(Graphics graphics, {int color, bool isHorizontal}) {
var from = isHorizontal ? Alignment.centerLeft : Alignment.bottomCenter;
var to = isHorizontal ? Alignment.centerRight : Alignment.topCenter;
graphics
.beginGradientFill(
GradientType.linear, [color, color], [1, 0], null, from, to)
.drawRect(0, 0, sw, sh)
.endFill();
}
Future<void> handleChangeHue(Color hueColor) async {
bgColor.graphics
.clear()
.beginFill(hueColor.value)
.drawRect(0, 0, sw, sh)
.endFill();
/// to avoid much memory impact generating textures on dragging.
/// so each 150ms create the Image snapshot
GTween.killTweensOf(_updateColorBytes);
GTween.delayedCall(0.15, _updateColorBytes);
}
Future<void> _updateColorBytes() async {
colorsBytes = await getImageBytes(colorsContainer);
updateColor();
}
void updateColor() {
if (colorsBytes == null) return;
_selectedColor = getPixelColor(
colorsBytes,
sw.toInt(),
sh.toInt(),
selector.x.toInt(),
selector.y.toInt(),
);
/// emit the event to update the UI.
pickerNotifier.value = _selectedColor;
// pickerMPS.emit1<Color>(ColorPickerEmitter.changeValue, _selectedColor);
}
}
class HueScene extends SceneRoot {
Shape colorSelector;
Shape arrowSelector;
Shape lineSelector;
Color _selectedColor;
double sw, sh;
ByteData colorsBytes;
HueScene() {
config(usePointer: true, autoUpdateAndRender: true);
}
@override
void addedToStage() {
var numHues = 20;
var hvsList = List.generate(numHues, (index) {
return HSVColor.fromAHSV(1, index / numHues * 360, 1, 1).toColor().value;
});
sw = stage.stageWidth;
sh = stage.stageHeight;
colorSelector = Shape();
colorSelector.graphics
.beginGradientFill(GradientType.linear, hvsList, null, null,
Alignment.bottomCenter, Alignment.topCenter)
.drawRoundRectComplex(0, 0, sw, sh, 0, 12, 0, 12)
.endFill();
final arrowSize = 5.0;
arrowSelector = Shape();
lineSelector = Shape();
lineSelector.graphics.beginFill(0xffffff).drawRect(0, 0, sw, 10);
lineSelector.alignPivot(Alignment.center);
lineSelector.x = sw / 2;
/// create the arrow shape first
arrowSelector.graphics.beginFill(0x0).drawPolygonFaces(0, 0, arrowSize, 3);
/// get the path.
final arrowPath = arrowSelector.graphics.getPaths();
/// clear the current graphics and apply the path with transforms.
arrowSelector.graphics
.clear()
.beginFill(0xfffffff)
.lineStyle(0, 0x0, 1)
.drawPath(arrowPath, -arrowSize, 0)
.drawPath(arrowPath, sw + arrowSize, 0, GxMatrix()..rotate(pi));
// lineSelector.alignPivot();
// lineSelector.x = sw / 2;
addChild(colorSelector);
addChild(arrowSelector);
addChild(lineSelector);
lineSelector.alpha = 0;
mouseChildren = false;
lineSelector.scaleX = 0;
stage.onMouseDown.add((input) {
// lineSelector.y = sh / 2;
GTween.killTweensOf(lineSelector);
lineSelector.height = 8;
lineSelector.tween(
duration: .8,
height: 2,
scaleX: 1,
alpha: 1,
ease: GEase.easeOutExpo,
);
stage.onMouseUp.addOnce((input) {
lineSelector.tween(
duration: .8,
scaleX: 0,
height: 0,
);
});
_updatePosition();
});
stage.onMouseMove.add(_onMouseMove);
/// get the image bytes from capturing the Shape snapshot.
/// so we can get the colors from the bytes List.
getImageBytes(colorSelector).then((value) {
colorsBytes = value;
updateColor();
});
}
void _onMouseMove(MouseInputData input) {
if (input.isPrimaryDown) {
_updatePosition();
}
}
void _updatePosition() {
lineSelector.y = arrowSelector.y = mouseY.clamp(0.0, sh - 1);
updateColor();
}
void updateColor() {
_selectedColor = getPixelColor(
colorsBytes,
sw.toInt(),
sh.toInt(),
0,
arrowSelector.y.toInt(),
);
/// emit the event to update the UI.
pickerMPS.emit1<Color>(ColorPickerEmitter.changeHue, _selectedColor);
}
}
/// we could just use global mps emitter from graphx.
final pickerMPS = ColorPickerEmitter();
/// still not widget builder for MPS... so we need a notifier to interact
/// with the Widgets.
final pickerNotifier = ValueNotifier(Colors.black);
class ColorPickerEmitter extends MPS {
static const changeHue = 'changeHue';
static const changeValue = 'changeValue';
}
Color getPixelColor(
ByteData rgbaImageData,
int imageWidth,
int imageHeight,
int x,
int y,
) {
final byteOffset = x * 4 + y * imageWidth * 4;
final r = rgbaImageData.getUint8(byteOffset);
final g = rgbaImageData.getUint8(byteOffset + 1);
final b = rgbaImageData.getUint8(byteOffset + 2);
final a = rgbaImageData.getUint8(byteOffset + 3);
return Color.fromARGB(a, r, g, b);
}
Future<ByteData> getImageBytes(DisplayObject object) async {
var texture = await object.createImageTexture(true, 1);
var data = texture.root.toByteData(format: ImageByteFormat.rawRgba);
// texture?.dispose();
// texture = null;
return data;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment