controlp5/src/controlP5/ColorWheel.java

600 lines
17 KiB
Java

package controlP5;
/**
* controlP5 is a processing gui library.
*
* 2006-2015 by Andreas Schlegel
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General
* Public License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place, Suite 330,
* Boston, MA 02111-1307 USA
*
* @author Andreas Schlegel (http://www.sojamo.de)
* @modified ##date##
* @version ##version##
*
*/
import java.util.HashMap;
import java.util.Map;
import processing.core.PApplet;
import processing.core.PGraphics;
public class ColorWheel extends Controller< ColorWheel > {
/* TODO _myColorValue should only be used internally,
* when broadcasting, a composed value based on the hsl
* and alpha value should be distributed, same goes for
* getValue. */
private int _myColorValue = 0xffffffff;
private final Map< String , PGraphics > _myColorResources;
private final float[] _myCursor;
private float scalar = 0.8f;
private int yoff = 10;
private boolean isInfo = false;
private Label _myInfoLabel;
private int drag = NONE;
private final static int NONE = -1;
private final static int SATURATION = 0;
private final static int COLOR = 1;
private final static int ALPHA = 2;
int _sideHandleHeight = 8;
private double[] hsl = new double[] { 1.0 , 1.0 , 1.0 };
// argb = int ( 0-255 , 0-255 , 0-255 , 0-255 )
// hue = double ( 0.0-1.0 ) 0-360
// saturation = double ( 0.0-1.0 ) 0-100%
// lightness = double ( 0.0-1.0 ) 0-100%
// brightness = double ( 0.0-1.0 ) 0-100%
public ColorWheel( ControlP5 theControlP5 , String theName ) {
this( theControlP5 , theControlP5.getDefaultTab( ) , theName , 0 , 0 , autoWidth , autoHeight );
theControlP5.register( theControlP5.papplet , theName , this );
}
public ColorWheel( ControlP5 theControlP5 , ControllerGroup< ? > theParent , String theName , int theX , int theY , int theWidth , int theHeight ) {
super( theControlP5 , theParent , theName , theX , theY , theWidth , theHeight );
_myColorResources = new HashMap< String , PGraphics >( );
_myColorResources.put( "default" , cp5.papplet.createGraphics( theWidth , theHeight ) );
_myCursor = new float[] { getWidth( ) / 2 , getHeight( ) / 2 };
_myCaptionLabel.align( LEFT , BOTTOM_OUTSIDE );
_myCaptionLabel.setPaddingX( 0 );
_myInfoLabel = new Label( cp5 , theName + "-info" );
_myInfoLabel.setPaddingX( 4 ).getStyle( ).marginTop = 4;
yoff = ( int ) ( getWidth( ) * 0.05 );
setColorResources( );
}
@Override public void onStartDrag( ) {
checkDrag( );
}
private void checkDrag( ) {
double x = getPointer( ).x( );
double y = getPointer( ).y( ) + yoff;
double xcenter = getWidth( ) / 2;
double ycenter = getHeight( ) / 2;
double d1 = ( ( getWidth( ) / 2 ) * scalar ) + 1;
double d = Math.sqrt( Math.pow( x - xcenter , 2 ) + Math.pow( y - ycenter , 2 ) );
double w = ( getWidth( ) - ( d1 * 2 ) ) / 2;
drag = NONE;
if ( d <= d1 ) {
drag = COLOR;
} else if ( x >= 0 && x <= w ) {
drag = SATURATION;
} else if ( x >= getWidth( ) - w && x <= getWidth( ) ) {
drag = ALPHA;
}
}
public void onEndDrag( ) {
drag = NONE;
}
@Override public void onDrag( ) {
switch ( drag ) {
case ( COLOR ):
double x = getPointer( ).x( );
double y = getPointer( ).y( ) + yoff;
double xcenter = getWidth( ) / 2;
double ycenter = getHeight( ) / 2;
double a = Math.atan2( y - ycenter , x - xcenter );
double d0 = getWidth( ) * 0.1;
double d1 = ( ( getWidth( ) / 2 ) * scalar ) + 1;
double d = Math.sqrt( Math.pow( x - xcenter , 2 ) + Math.pow( y - ycenter , 2 ) );
if ( d >= d1 - 1 ) {
x = ( xcenter + Math.cos( a ) * d1 );
y = ( ycenter + Math.sin( a ) * d1 );
} else if ( d <= d0 ) {
x = ( xcenter + Math.cos( a ) * d0 );
y = ( ycenter + Math.sin( a ) * d0 );
}
set( _myCursor , ( float ) x , ( float ) y );
int xx = ( int ) x;
int yy = ( int ) y;
double[] t = RGBtoHSL( _myColorResources.get( "default" ).get( xx , yy ) );
hsl[ 0 ] = t[ 0 ];
hsl[ 2 ] = t[ 2 ];
_myColorValue = HSLtoRGB( hsl );
setValue( _myColorValue );
break;
case ( SATURATION ):
float s1 = ( getHeight( ) - ( yoff * 2 ) - _sideHandleHeight );
setSaturation( map( getPointer( ).y( ) , 0 , s1 , 1.0 , 0.0 ) );
_myColorValue = HSLtoRGB( hsl );
setValue( _myColorValue );
break;
case ( ALPHA ):
float a1 = ( getHeight( ) - ( yoff * 2 ) - _sideHandleHeight );
setAlpha( ( int ) map( getPointer( ).y( ) , 0 , a1 , 255 , 0 ) );
_myColorValue = HSLtoRGB( hsl );
setValue( _myColorValue );
break;
}
}
@Override public void onPress( ) {
checkDrag( );
}
@Override public void onRelease( ) {
onDrag( );
}
public ColorWheel scrolled( int theRotationValue ) {
if ( isVisible ) {
double x = getPointer( ).x( );
double d1 = ( ( getWidth( ) / 2 ) * scalar ) + 1;
double w = ( getWidth( ) - ( d1 * 2 ) ) / 2;
if ( x >= 0 && x <= w ) {
setSaturation( hsl[ 1 ] + theRotationValue * 0.01 );
_myColorValue = HSLtoRGB( hsl );
setValue( _myColorValue );
} else if ( x >= getWidth( ) - w && x <= getWidth( ) ) {
setAlpha( a( ) + theRotationValue );
}
}
return this;
}
private void setColorResources( ) {
/* for now there is only a default resource but this
* can be extended to support other color models in
* the future. */
PGraphics buffer = _myColorResources.get( "default" );
buffer.beginDraw( );
buffer.background( 0 , 0 );
int w = buffer.width;
int h = buffer.height;
float[] center = new float[] { w / 2 , h / 2 };
int inner_radius = ( int ) ( buffer.width * 0.1 );
int outer_radius = ( int ) ( buffer.width * scalar / 2 );
buffer.fill( 0 );
buffer.ellipseMode( CENTER );
buffer.ellipse( buffer.width / 2 , buffer.height / 2 , buffer.width * scalar + 4 , buffer.width * scalar + 4 );
buffer.fill( 255 );
buffer.ellipse( buffer.width / 2 , buffer.height / 2 , ( inner_radius + 1 ) * 2 , ( inner_radius + 1 ) * 2 );
for ( int y = 0 ; y < h ; y++ ) {
int dy = ( int ) ( y( center ) - y );
for ( int x = 0 ; x < w ; x++ ) {
int dx = ( int ) ( x( center ) - x );
double dist = Math.sqrt( dx * dx + dy * dy );
if ( dist >= inner_radius && dist <= outer_radius ) {
double theta = Math.atan2( dy , dx );
// theta can go from -pi to pi
double hue = ( theta + PI ) / ( TWO_PI );
double sat , val;
if ( dist < ( inner_radius + ( outer_radius - inner_radius ) / 2 ) ) {
sat = map( dist , inner_radius , outer_radius , 0 , 2 );
val = 1;
} else {
sat = 1;
val = map( dist , inner_radius , outer_radius , 2 , 0 );
}
buffer.set( x , y , HSVtoRGB( hue , sat , val ) );
}
}
}
buffer.endDraw( );
}
public void setHue( double theH ) {
hsl[ 0 ] = Math.max( 0 , Math.min( 1 , theH ) );
}
public void setSaturation( double theS ) {
hsl[ 1 ] = Math.max( 0 , Math.min( 1 , theS ) );
}
public void setLightness( double theL ) {
hsl[ 2 ] = Math.max( 0 , Math.min( 1 , theL ) );
}
public ColorWheel setHSL( double theH , double theS , double theL ) {
setHue( theH );
setSaturation( theS );
setLightness( theL );
return this;
}
public int getRGB( ) {
return _myColorValue;
}
public ColorWheel setRGB( int theColor ) {
double[] t = RGBtoHSL( theColor );
hsl[ 0 ] = t[ 0 ];
hsl[ 2 ] = t[ 2 ];
float theta = ( float ) ( t[ 0 ] * TWO_PI ) - PI;
float d0 = getWidth( ) * 0.1f;
float d1 = ( ( getWidth( ) / 2 ) * scalar ) + 1f;
float s = ( float ) map( t[ 2 ] , 0f , 1f , d1 , d0 );
float x = _myColorResources.get( "default" ).width / 2 - ( float ) Math.cos( theta ) * s;
float y = _myColorResources.get( "default" ).height / 2 - ( float ) Math.sin( theta ) * s;
set( _myCursor , x , y );
setSaturation( t[ 1 ] );
// TODO resolve rounding error issue as reported here https://github.com/sojamo/controlp5/issues/21
_myColorValue = HSLtoRGB( hsl );
setValue( _myColorValue );
return this;
}
public ColorWheel setAlpha( int theAlpha ) {
/* TODO */
return this;
}
/**
* @exclude
*/
@Override @ControlP5.Invisible public ColorWheel updateDisplayMode( int theMode ) {
return updateViewMode( theMode );
}
/**
* @exclude
*/
@ControlP5.Invisible public ColorWheel updateViewMode( int theMode ) {
_myDisplayMode = theMode;
switch ( theMode ) {
case ( DEFAULT ):
case ( IMAGE ):
case ( CUSTOM ):
_myControllerView = new ColorWheelView( );
default:
break;
}
return this;
}
public int a( ) {
int a = ( _myColorValue & 0xff000000 ) >> 24;
return ( a < 0 ) ? 255 : a;
}
public int r( ) {
return ( _myColorValue & 0x00ff0000 ) >> 16;
}
public int g( ) {
return ( _myColorValue & 0x0000ff00 ) >> 8;
}
public int b( ) {
return ( _myColorValue & 0x000000ff ) >> 0;
}
private class ColorWheelView implements ControllerView< ColorWheel > {
public void display( PGraphics theGraphics , ColorWheel theController ) {
PGraphics buffer = _myColorResources.get( "default" );
theGraphics.fill( 0 , 100 );
theGraphics.rect( 0 , 0 , getWidth( ) , getHeight( ) );
theGraphics.ellipseMode( PApplet.CENTER );
theGraphics.pushMatrix( );
theGraphics.translate( 0 , -yoff );
theGraphics.image( buffer , 0 , 0 );
theGraphics.pushMatrix( );
theGraphics.translate( x( _myCursor ) , y( _myCursor ) );
theGraphics.strokeWeight( 2 );
theGraphics.noFill( );
theGraphics.stroke( 255 , 40 );
theGraphics.ellipse( 1 , 1 , 10 , 10 );
theGraphics.stroke( 250 );
theGraphics.fill( _myColorValue );
theGraphics.ellipse( 0 , 0 , 10 , 10 );
theGraphics.popMatrix( );
theGraphics.noStroke( );
theGraphics.translate( 0 , -yoff );
theGraphics.fill( HSLtoRGB( hsl[ 0 ] , hsl[ 1 ] , hsl[ 2 ] ) );
theGraphics.rect( 0 , getHeight( ) , getWidth( ) , yoff * 2 );
theGraphics.popMatrix( );
if ( isLabelVisible ) {
_myCaptionLabel.draw( theGraphics , 0 , 0 , theController );
}
if ( isInfo ) {
_myInfoLabel.setText( String.format( "RGB %d %d %d\nALPHA %d\nHSL %d %.2f %.2f " , r( ) , g( ) , b( ) , a( ) , ( int ) ( hsl[ 0 ] * 360 ) , hsl[ 1 ] , hsl[ 2 ] ) );
_myInfoLabel.draw( theGraphics , 0 , 0 , theController );
}
theGraphics.fill( 255 );
theGraphics.pushMatrix( );
int s = _sideHandleHeight / 2;
float v = ( getHeight( ) - ( yoff * 2 ) - _sideHandleHeight );
theGraphics.translate( 2 , ( int ) map( hsl[ 1 ] , 1 , 0 , 0 , v ) );
theGraphics.triangle( 0 , s , s , 0 , s , _sideHandleHeight );
theGraphics.popMatrix( );
/* TODO alpha handle theGraphics.pushMatrix( );
* theGraphics.translate( getWidth( ) - s - 2 ,
* ( int ) map( a( ) , 255 , 0 , 0 , v ) );
* theGraphics.triangle( s , s , 0 , 0 , 0 ,
* _sideHandleHeight ); theGraphics.popMatrix(
* ); */
}
}
public double[] RGBtoHSL( int theRGB ) {
return RGBtoHSL( theRGB >> 16 & 0xff , theRGB >> 8 & 0xff , theRGB >> 0 & 0xff );
}
/**
*
* @param theR value between 0 and 255
* @param theG value between 0 and 255
* @param theB value between 0 and 255
* @return double[] values h,s,l are between 0-1
*/
public double[] RGBtoHSL( int theR , int theG , int theB ) {
double[] rgb = new double[ 3 ];
rgb[ 0 ] = theR / 255.0;
rgb[ 1 ] = theG / 255.0;
rgb[ 2 ] = theB / 255.0;
double max = Math.max( rgb[ 0 ] , Math.max( rgb[ 1 ] , rgb[ 2 ] ) );
double min = Math.min( rgb[ 0 ] , Math.min( rgb[ 1 ] , rgb[ 2 ] ) );
double h = ( max + min ) / 2;
double s = ( max + min ) / 2;
double l = ( max + min ) / 2;
if ( max == min ) {
h = s = 0; // achromatic
} else {
double d = max - min;
s = l > 0.5 ? ( d / ( 2 - max - min ) ) : ( d / ( max + min ) );
if ( max == rgb[ 0 ] ) {
h = ( rgb[ 1 ] - rgb[ 2 ] ) / d + ( rgb[ 1 ] < rgb[ 2 ] ? 6 : 0 );
} else if ( max == rgb[ 1 ] ) {
h = ( rgb[ 2 ] - rgb[ 0 ] ) / d + 2;
} else if ( max == rgb[ 2 ] ) {
h = ( rgb[ 0 ] - rgb[ 1 ] ) / d + 4;
}
h /= 6;
}
return new double[] { h , s , l };
}
public int HSVtoRGB( double[] hsv ) {
return HSVtoRGB( hsv[ 0 ] , hsv[ 1 ] , hsv[ 2 ] );
}
/**
*
* @param H value between 0-1
* @param S value between 0-1
* @param V value between 0-1
* @return int
*/
public int HSVtoRGB( double H , double S , double V ) {
/* http://viziblr.com/news/2011/12/1/drawing-a-color-
* hue-wheel-with-c.html */
double[] rgb = new double[ 3 ];
if ( H == 1.0 ) {
H = 0.0;
}
double step = 1.0 / 6.0;
double vh = H / step;
int i = ( int ) Math.floor( vh );
double f = vh - i;
double p = V * ( 1.0 - S );
double q = V * ( 1.0 - ( S * f ) );
double t = V * ( 1.0 - ( S * ( 1.0 - f ) ) );
switch ( i ) {
case 0: {
rgb[ 0 ] = V;
rgb[ 1 ] = t;
rgb[ 2 ] = p;
break;
}
case 1: {
rgb[ 0 ] = q;
rgb[ 1 ] = V;
rgb[ 2 ] = p;
break;
}
case 2: {
rgb[ 0 ] = p;
rgb[ 1 ] = V;
rgb[ 2 ] = t;
break;
}
case 3: {
rgb[ 0 ] = p;
rgb[ 1 ] = q;
rgb[ 2 ] = V;
break;
}
case 4: {
rgb[ 0 ] = t;
rgb[ 1 ] = p;
rgb[ 2 ] = V;
break;
}
case 5: {
rgb[ 0 ] = V;
rgb[ 1 ] = p;
rgb[ 2 ] = q;
break;
}
default: {
// not possible - if we get here it is an
// internal error
// throw new ArgumentException();
System.out.println( "hsv to rgb not possible" );
}
}
return ( a( ) << 24 ) | ( ( int ) ( rgb[ 0 ] * 255 ) << 16 ) | ( ( int ) ( rgb[ 1 ] * 255 ) << 8 ) | ( int ) ( rgb[ 2 ] * 255 );
}
public final double[] RGBtoHSV( final int c ) {
return RGBtoHSV( ( c & 0xff0000 ) >> 16 , ( c & 0x00ff00 ) >> 8 , ( c & 0x0000ff ) >> 0 );
}
/**
*
* @param theR value between 0 and 255
* @param theG value between 0 and 255
* @param theB value between 0 and 255
* @return hsv [ hue (0-1) sat (0-1) val (0-1) ]
*/
public final double[] RGBtoHSV( final int theR , final int theG , final double theB ) {
double hue = 0;
double sat = 0;
double val = 0;
final double r = theR / 255.0;
final double g = theG / 255.0;
final double b = theB / 255.0;
double minRGB = Math.min( r , Math.min( g , b ) );
double maxRGB = Math.max( r , Math.max( g , b ) );
// Black-gray-white
if ( minRGB == maxRGB ) {
return new double[] { 0 , 0 , minRGB };
}
// Colors other than black-gray-white:
double d = ( r == minRGB ) ? g - b : ( ( b == minRGB ) ? r - g : b - r );
double h = ( r == minRGB ) ? 3 : ( ( b == minRGB ) ? 1 : 5 );
hue = map( ( h - ( d / ( maxRGB - minRGB ) ) ) , 0 , 6.0 , 0 , 1.0 );
sat = ( maxRGB - minRGB ) / maxRGB;
val = maxRGB;
return new double[] { hue , sat , val };
}
public int HSLtoRGB( final double[] theHSL ) {
if ( theHSL.length == 3 ) {
return HSLtoRGB( theHSL[ 0 ] , theHSL[ 1 ] , theHSL[ 2 ] );
} else {
String message = "HSLtoRGB(double[]) a length of 3 is expected. ";
throw new IllegalArgumentException( message );
}
}
/**
*
* @param h value between 0 and 360
* @param s value between 0 and 100
* @param l) value between 0 and 100
* @param alpha value between 0 and 1
* @return
*/
public int HSLtoRGB( final double h , double s , double l ) {
if ( h < 0.0 || h > 1.0 ) {
String message = "Color parameter outside of expected range - Hue ( 0.0 - 1.0 )";
throw new IllegalArgumentException( message );
}
if ( s < 0.0 || s > 1.0 ) {
String message = "Color parameter outside of expected range - Saturation ( 0.0 - 1.0 )";
throw new IllegalArgumentException( message );
}
if ( l < 0.0 || l > 1.0 ) {
String message = "Color parameter outside of expected range - Luminance ( 0.0 - 1.0 )";
throw new IllegalArgumentException( message );
}
double q = 0;
if ( l < 0.5 ) {
q = l * ( 1 + s );
} else {
q = ( l + s ) - ( s * l );
}
double p = 2 * l - q;
double r = Math.max( 0 , HueToRGB( p , q , h + ( 1.0f / 3.0f ) ) );
double g = Math.max( 0 , HueToRGB( p , q , h ) );
double b = Math.max( 0 , HueToRGB( p , q , h - ( 1.0f / 3.0f ) ) );
return ( 255 << 24 ) | ( ( int ) ( r * 255 ) << 16 ) | ( ( int ) ( g * 255 ) << 8 ) | ( int ) ( b * 255 );
}
private static double HueToRGB( double p , double q , double h ) {
if ( h < 0 )
h += 1;
if ( h > 1 )
h -= 1;
if ( 6 * h < 1 ) {
return p + ( ( q - p ) * 6 * h );
}
if ( 2 * h < 1 ) {
return q;
}
if ( 3 * h < 2 ) {
return p + ( ( q - p ) * 6 * ( ( 2.0f / 3.0f ) - h ) );
}
return p;
}
private final double map( double val , double a1 , double a2 , double b1 , double b2 ) {
return b1 + ( b2 - b1 ) * ( ( val - a1 ) / ( a2 - a1 ) );
}
}