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

Enum-ify Theme. #89

Merged
merged 8 commits into from
Jan 28, 2024
Merged
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
27 changes: 19 additions & 8 deletions app/src/main/java/com/nicobrailo/pianoli/Piano.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,32 +165,43 @@ int pos_to_key_idx(float pos_x, float pos_y) {
if (pos_y > keys_flats_height) return big_key_idx;

// Check if press is inside rect of flat key
Key flat = get_area_for_flat_key(big_key_idx);
Key flat = getAreaForSmallKey(big_key_idx + 1);
if (flat.contains(pos_x, pos_y)) return big_key_idx + 1;

if (big_key_idx > 0) {
Key prev_flat = get_area_for_flat_key(big_key_idx - 2);
Key prev_flat = getAreaForSmallKey(big_key_idx - 1);
if (prev_flat.contains(pos_x, pos_y)) return big_key_idx - 1;
}

// If not in the current or previous flat, it must be a hit in the big key
return big_key_idx;
}

Key get_area_for_key(int key_idx) {
int x_i = key_idx / 2 * keys_width;
@NonNull
Key getAreaForKey(int keyIdx) {
if ((keyIdx & 1) == 0) { // even positions are the full, big keys
return getAreaForBigKey(keyIdx);
} else {
return getAreaForSmallKey(keyIdx); // odd positions are the small/black/flat keys.
}
}

@NonNull
private Key getAreaForBigKey(int keyIdx) {
int x_i = keyIdx / 2 * keys_width;
return new Key(x_i, x_i + keys_width, 0, keys_height);
}

Key get_area_for_flat_key(int key_idx) {
final int octave_idx = (key_idx / 2) % 7;
if (octave_idx == 2 || octave_idx == 6) {
@NonNull
private Key getAreaForSmallKey(int keyIdx) {
final int octaveIdx = (keyIdx / 2) % 7;
if (octaveIdx == 2 || octaveIdx == 6) {
// Keys without flat get a null-area
return Key.CANT_TOUCH_THIS;
}

final int offset = keys_width - (keys_flat_width / 2);
int x_i = (key_idx / 2) * keys_width + offset;
int x_i = (keyIdx / 2) * keys_width + offset;
return new Key(x_i, x_i + keys_flat_width, 0, keys_flats_height);
}
}
48 changes: 24 additions & 24 deletions app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ public PianoCanvas(Context context, AttributeSet as, int defStyle) {
public void reInitPiano(Context context, String prefSoundset) {
Log.i("PianOli::PianoCanvas", "re-initialising Piano");
this.piano = new Piano(screen_size_x, screen_size_y);
this.theme = Theme.fromPreferences(context);

String prefTheme = Preferences.selectedTheme(context);
this.theme = Theme.fromPreference(prefTheme);

// for config trigger updates
piano.addListener(appConfigTrigger);
Expand Down Expand Up @@ -142,24 +144,6 @@ private static void resetCanvas(Canvas canvas) {
canvas.drawPaint(p);
}

private void drawBigKeys(Canvas canvas) {
for (int i = 0; i < piano.get_keys_count(); i += 2) {
Paint paint = new Paint();
paint.setColor(theme.getColorForKey(i, piano.is_key_pressed(i)));
draw_key(canvas, piano.get_area_for_key(i), paint);
}
}

private void drawSmallKeys(Canvas canvas) {
for (int i = 1; i < piano.get_keys_count(); i += 2) {
Paint paint = new Paint();
paint.setColor(piano.is_key_pressed(i) ? Color.GRAY : 0xFF333333);
if (piano.get_area_for_flat_key(i) != null) {
draw_key(canvas, piano.get_area_for_flat_key(i), paint);
}
}
}

/**
* Overlays gear icons onto the currently-held and next expected flat keys.
*/
Expand All @@ -175,7 +159,15 @@ void drawConfigGears(Canvas androidCanvas) {
draw_icon_on_black_key(androidCanvas, gearIcon, appConfigTrigger.getNextExpectedKey(), normalSize, normalSize);
}

void draw_key(final Canvas canvas, final Key rect, final Paint p) {
void drawKey(final Canvas canvas, final int i) {
Key rect = piano.getAreaForKey(i);
if (rect == Key.CANT_TOUCH_THIS) {
return; // don't waste performance drawing the skipped black keys.
}

Paint p = new Paint();
p.setColor(theme.getColorForKey(i, piano.is_key_pressed(i)));

// Draw the main (solid) background of the key.

Rect r = new Rect();
Expand Down Expand Up @@ -252,7 +244,7 @@ void draw_key(final Canvas canvas, final Key rect, final Paint p) {
*/
void draw_icon_on_black_key(final Canvas canvas, final Drawable icon, int key_idx,
final int icon_width, final int icon_height) {
final Key key = piano.get_area_for_flat_key(key_idx);
final Key key = piano.getAreaForKey(key_idx);
int icon_x = ((key.x_f - key.x_i) / 2) + key.x_i;
int icon_y = icon_height;

Expand Down Expand Up @@ -284,9 +276,17 @@ public void redraw(SurfaceHolder surfaceHolder) {
if (canvas == null) return;

resetCanvas(canvas);
drawBigKeys(canvas);
// Small keys drawn after big keys to ensure z-index
drawSmallKeys(canvas);

// draw main, big keys (even key index)
for (int i = 0; i < piano.get_keys_count(); i += 2) {
drawKey(canvas, i);
}

// Small keys drawn after big keys to ensure z-index (odd key index)
for (int i = 1; i < piano.get_keys_count(); i += 2) {
drawKey(canvas, i);
}

// Gear icons drawn after small keys, since they go on top of those.
drawConfigGears(canvas);

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/nicobrailo/pianoli/Preferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class Preferences {
private final static String PREF_SELECTED_SOUND_SET = "selectedSoundSet";
private final static String PREF_SELECTED_MELODIES = "selectedMelodies";
private final static String PREF_ENABLE_MELODIES = "enableMelodies";
private static final String DEFAULT_THEME = "rainbow";
public static final String DEFAULT_THEME = "rainbow";
private final static String PREF_THEME = "theme";

/**
Expand Down
180 changes: 120 additions & 60 deletions app/src/main/java/com/nicobrailo/pianoli/Theme.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,105 @@
package com.nicobrailo.pianoli;

import android.content.Context;
import android.graphics.Color;

import androidx.core.graphics.ColorUtils;

public class Theme {

/**
* Switchable key-colouring decisions for {@link PianoCanvas}.
*
* <p>
* Whenever {@link PianoCanvas} renders a key, it asks the current <code>Theme</code>-variant for the paint colour.
* This allows us to switch palettes via preferences.
* </p>
*
* @see Preferences#selectedTheme(android.content.Context)
* @see PianoCanvas#drawKey(android.graphics.Canvas, int)
*/
public enum Theme {
/**
* Boomwhackers are colour-coded pipes that produce a (tuned) note when hit.
* Their popularity in educational circles cemented their colour-to-note mapping as a de-facto international standard.
*
* <p>
* See also: <a href="https://en.wikipedia.org/wiki/Boomwhacker">wikipedia: Boomwhacker</a>, and
* <a href="https://boomwhackers.com/">boomwhackers.com</a>.
* </p>
*
*
* <p>
* Note that light green, orange and yellow have higher lightness than other colors,
* so adding just a little white doesn't have the desired effect.
* That is why they have a larger proportion of white added in.
* </p>
*/
BOOMWHACKER(new KeyColor[] {
KeyColor.createLighterWhenPressed(Color.rgb(220, 0, 0), 0.5f), // Red
KeyColor.createLighterWhenPressed(Color.rgb(255, 135, 0), 0.6f), // Orange
KeyColor.createLighterWhenPressed(Color.rgb(255, 255, 0), 0.75f), // Yellow
KeyColor.createLighterWhenPressed(Color.rgb(80, 220, 20), 0.6f), // Light Green
KeyColor.createLighterWhenPressed(Color.rgb(0, 150, 150), 0.5f), // Dark Green
KeyColor.createLighterWhenPressed(Color.rgb(95, 70, 165), 0.5f), // Purple
KeyColor.createLighterWhenPressed(Color.rgb(213, 43, 149), 0.5f), // Pink
}),

/**
* Soft pastel tones, derived from <a href="https://colorbrewer2.org/#type=qualitative&scheme=Pastel1&n=8">Colorbrewer2.org: Pastel</a>.
*/
PASTEL(new KeyColor[] {
KeyColor.createLighterWhenPressed(0xfffbb4ae, 0.5f), // dark pink
KeyColor.createLighterWhenPressed(0xffb3cde3, 0.5f), // powder blue
KeyColor.createLighterWhenPressed(0xffccebc5, 0.5f), // pistachio green
KeyColor.createLighterWhenPressed(0xffdecbe4, 0.5f), // lavender
KeyColor.createLighterWhenPressed(0xfffed9a6, 0.5f), // orange
KeyColor.createLighterWhenPressed(0xffffffcc, 0.5f), // pale yellow
KeyColor.createLighterWhenPressed(0xffe5d8bd, 0.5f), // light pink
}),

/**
* All the colours of the rainbow, C1 dark blue, C2 red, then looping back to blue.
*/
RAINBOW(new KeyColor[] {
KeyColor.createLighterWhenPressed(0xff001caf, 0.5f), // darkblue
KeyColor.createLighterWhenPressed(0xff0099ff, 0.5f), // lightblue
KeyColor.createLighterWhenPressed(0xff63c624, 0.5f), // darkgreen
KeyColor.createLighterWhenPressed(0xffbde53d, 0.5f), // lightgreen
KeyColor.createLighterWhenPressed(0xfffcc000, 0.5f), // yellow
KeyColor.createLighterWhenPressed(0xffff810a, 0.5f), // lightorange
KeyColor.createLighterWhenPressed(0xffff5616, 0.5f), // darkorange
KeyColor.createLighterWhenPressed(0xffd51016, 0.5f), // red
}),

/**
* "classic" Ivory and Black.
*/
BLACK_AND_WHITE(new KeyColor[] {
new KeyColor( // white, lighter
Color.rgb(240, 240, 240), // normal: slightly muted white;
Color.rgb(200, 200, 200) // darker gray when pressed
)
}); // note that the black flat keys are implicit and hardcoded for all themes at the moment.

/**
* Prefix for preferences-values and translation strings.
*
* <p>
* Often used implicitly, so don't forget to do full-text searches across the project when changing this.
* </p>
*/
public static final String PREFIX = "theme_";

/**
* The sequence of colors to render; repeats from the start if there are more keys than array entries.
*/
private final KeyColor[] colors;

public static Theme fromPreferences(Context context) {
String selectedTheme = Preferences.selectedTheme(context);
public static Theme fromPreference(String selectedTheme) {
// defensive programming: if we ever mess up our preferences handling, it's better to fall back to default,
// than to crash the app.
if (selectedTheme == null) {
return RAINBOW;
}

switch (selectedTheme) {
case "black_and_white":
return BLACK_AND_WHITE;
Expand All @@ -27,70 +115,43 @@ public static Theme fromPreferences(Context context) {
}
}

/**
* Note that light green, orange and yellow have higher lightness than other colors,
* so adding just a little white doesn't have the desired effect.
* That is why they have a larger proportion of white added in.
*/
private static final Theme BOOMWHACKER = new Theme(
new KeyColor[] {
KeyColor.createLighterWhenPressed(Color.rgb(220, 0, 0), 0.5f), // Red
KeyColor.createLighterWhenPressed(Color.rgb(255, 135, 0), 0.6f), // Orange
KeyColor.createLighterWhenPressed(Color.rgb(255, 255, 0), 0.75f), // Yellow
KeyColor.createLighterWhenPressed(Color.rgb(80, 220, 20), 0.6f), // Light Green
KeyColor.createLighterWhenPressed(Color.rgb(0, 150, 150), 0.5f), // Dark Green
KeyColor.createLighterWhenPressed(Color.rgb(95, 70, 165), 0.5f), // Purple
KeyColor.createLighterWhenPressed(Color.rgb(213, 43, 149), 0.5f), // Pink
}
);

private static final Theme PASTEL = new Theme(
new KeyColor[] {
// https://colorbrewer2.org/#type=qualitative&scheme=Pastel1&n=8
KeyColor.createLighterWhenPressed(0xfffbb4ae, 0.5f),
KeyColor.createLighterWhenPressed(0xffb3cde3, 0.5f),
KeyColor.createLighterWhenPressed(0xffccebc5, 0.5f),
KeyColor.createLighterWhenPressed(0xffdecbe4, 0.5f),
KeyColor.createLighterWhenPressed(0xfffed9a6, 0.5f),
KeyColor.createLighterWhenPressed(0xffffffcc, 0.5f),
KeyColor.createLighterWhenPressed(0xffe5d8bd, 0.5f),
}
);

private static final Theme RAINBOW = new Theme(
new KeyColor[] {
KeyColor.createLighterWhenPressed(0xff001caf, 0.5f), // darkblue
KeyColor.createLighterWhenPressed(0xff0099ff, 0.5f), // lightblue
KeyColor.createLighterWhenPressed(0xff63c624, 0.5f), // darkgreen
KeyColor.createLighterWhenPressed(0xffbde53d, 0.5f), // lightgreen
KeyColor.createLighterWhenPressed(0xfffcc000, 0.5f), // yellow
KeyColor.createLighterWhenPressed(0xffff810a, 0.5f), // lightorange
KeyColor.createLighterWhenPressed(0xffff5616, 0.5f), // darkorange
KeyColor.createLighterWhenPressed(0xffd51016, 0.5f), // red
}
);

private static final Theme BLACK_AND_WHITE = new Theme(
new KeyColor[] {
new KeyColor(
Color.rgb(240, 240, 240),
Color.rgb(200, 200, 200)
)
}
);

private Theme(KeyColor[] colors) {


Theme(KeyColor[] colors) {
this.colors = colors;
}

public int getColorForKey(int keyIndex, boolean isPressed) {
final int col_idx = (keyIndex / 2) % colors.length;
if ((keyIndex & 1) == 1) { // odd index = black/flat/small key
return isPressed ? Color.GRAY : 0xFF333333; // hardcoded "black" for now, but theme-able in the future.
}

final int col_idx = (keyIndex / 2) % colors.length; // divide by two to skip 'flat'/black keys at odd positions.
final KeyColor color = colors[col_idx];
return isPressed ? color.pressed : color.normal;
}

/**
* The render-colours for a big piano key: {@link #normal} and {@link #pressed}.
*
* <p>
* Note that this only applies to big keys, the 'flat' keys are always black.
* </p>
*/
private static class KeyColor {
/** The normal rendering color, when the key is inactive */
public final int normal;

/**
* The pressed/touched color of a key.
*
* <p>
* Not automatically derived from {@link #normal}, because different hues need different amounts of
* real lightening for the same amount of subjective lightening.
* </p>
*
* @see #createLighterWhenPressed(int, float)
*/
public final int pressed;

public KeyColor(int normal, int pressed) {
Expand All @@ -102,5 +163,4 @@ public static KeyColor createLighterWhenPressed(int color, float blendWhiteFacto
return new KeyColor(color, ColorUtils.blendARGB(color, Color.WHITE, blendWhiteFactor));
}
}

}
Loading