Last active
August 29, 2021 12:29
-
-
Save roipeker/6e7d5b30f6b022196bc98e2db14676a2 to your computer and use it in GitHub Desktop.
GraphX Color Picker (based on SuperDeclarative video).
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
/// 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