Target.java

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.palette.graphics;

import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;

/**
 * A class which allows custom selection of colors in a {@link Palette}'s generation. Instances
 * can be created via the {@link Builder} class.
 *
 * <p>To use the target, use the {@link Palette.Builder#addTarget(Target)} API when building a
 * Palette.</p>
 */
public final class Target {

    private static final float TARGET_DARK_LUMA = 0.26f;
    private static final float MAX_DARK_LUMA = 0.45f;

    private static final float MIN_LIGHT_LUMA = 0.55f;
    private static final float TARGET_LIGHT_LUMA = 0.74f;

    private static final float MIN_NORMAL_LUMA = 0.3f;
    private static final float TARGET_NORMAL_LUMA = 0.5f;
    private static final float MAX_NORMAL_LUMA = 0.7f;

    private static final float TARGET_MUTED_SATURATION = 0.3f;
    private static final float MAX_MUTED_SATURATION = 0.4f;

    private static final float TARGET_VIBRANT_SATURATION = 1f;
    private static final float MIN_VIBRANT_SATURATION = 0.35f;

    private static final float WEIGHT_SATURATION = 0.24f;
    private static final float WEIGHT_LUMA = 0.52f;
    private static final float WEIGHT_POPULATION = 0.24f;

    static final int INDEX_MIN = 0;
    static final int INDEX_TARGET = 1;
    static final int INDEX_MAX = 2;

    static final int INDEX_WEIGHT_SAT = 0;
    static final int INDEX_WEIGHT_LUMA = 1;
    static final int INDEX_WEIGHT_POP = 2;

    /**
     * A target which has the characteristics of a vibrant color which is light in luminance.
    */
    public static final Target LIGHT_VIBRANT;

    /**
     * A target which has the characteristics of a vibrant color which is neither light or dark.
     */
    public static final Target VIBRANT;

    /**
     * A target which has the characteristics of a vibrant color which is dark in luminance.
     */
    public static final Target DARK_VIBRANT;

    /**
     * A target which has the characteristics of a muted color which is light in luminance.
     */
    public static final Target LIGHT_MUTED;

    /**
     * A target which has the characteristics of a muted color which is neither light or dark.
     */
    public static final Target MUTED;

    /**
     * A target which has the characteristics of a muted color which is dark in luminance.
     */
    public static final Target DARK_MUTED;

    static {
        LIGHT_VIBRANT = new Target();
        setDefaultLightLightnessValues(LIGHT_VIBRANT);
        setDefaultVibrantSaturationValues(LIGHT_VIBRANT);

        VIBRANT = new Target();
        setDefaultNormalLightnessValues(VIBRANT);
        setDefaultVibrantSaturationValues(VIBRANT);

        DARK_VIBRANT = new Target();
        setDefaultDarkLightnessValues(DARK_VIBRANT);
        setDefaultVibrantSaturationValues(DARK_VIBRANT);

        LIGHT_MUTED = new Target();
        setDefaultLightLightnessValues(LIGHT_MUTED);
        setDefaultMutedSaturationValues(LIGHT_MUTED);

        MUTED = new Target();
        setDefaultNormalLightnessValues(MUTED);
        setDefaultMutedSaturationValues(MUTED);

        DARK_MUTED = new Target();
        setDefaultDarkLightnessValues(DARK_MUTED);
        setDefaultMutedSaturationValues(DARK_MUTED);
    }

    final float[] mSaturationTargets = new float[3];
    final float[] mLightnessTargets = new float[3];
    final float[] mWeights = new float[3];
    boolean mIsExclusive = true; // default to true

    Target() {
        setTargetDefaultValues(mSaturationTargets);
        setTargetDefaultValues(mLightnessTargets);
        setDefaultWeights();
    }

    Target(@NonNull Target from) {
        System.arraycopy(from.mSaturationTargets, 0, mSaturationTargets, 0,
                mSaturationTargets.length);
        System.arraycopy(from.mLightnessTargets, 0, mLightnessTargets, 0,
                mLightnessTargets.length);
        System.arraycopy(from.mWeights, 0, mWeights, 0, mWeights.length);
    }

    /**
     * The minimum saturation value for this target.
     */
    @FloatRange(from = 0, to = 1)
    public float getMinimumSaturation() {
        return mSaturationTargets[INDEX_MIN];
    }

    /**
     * The target saturation value for this target.
     */
    @FloatRange(from = 0, to = 1)
    public float getTargetSaturation() {
        return mSaturationTargets[INDEX_TARGET];
    }

    /**
     * The maximum saturation value for this target.
     */
    @FloatRange(from = 0, to = 1)
    public float getMaximumSaturation() {
        return mSaturationTargets[INDEX_MAX];
    }

    /**
     * The minimum lightness value for this target.
     */
    @FloatRange(from = 0, to = 1)
    public float getMinimumLightness() {
        return mLightnessTargets[INDEX_MIN];
    }

    /**
     * The target lightness value for this target.
     */
    @FloatRange(from = 0, to = 1)
    public float getTargetLightness() {
        return mLightnessTargets[INDEX_TARGET];
    }

    /**
     * The maximum lightness value for this target.
     */
    @FloatRange(from = 0, to = 1)
    public float getMaximumLightness() {
        return mLightnessTargets[INDEX_MAX];
    }

    /**
     * Returns the weight of importance that this target places on a color's saturation within
     * the image.
     *
     * <p>The larger the weight, relative to the other weights, the more important that a color
     * being close to the target value has on selection.</p>
     *
     * @see #getTargetSaturation()
     */
    public float getSaturationWeight() {
        return mWeights[INDEX_WEIGHT_SAT];
    }

    /**
     * Returns the weight of importance that this target places on a color's lightness within
     * the image.
     *
     * <p>The larger the weight, relative to the other weights, the more important that a color
     * being close to the target value has on selection.</p>
     *
     * @see #getTargetLightness()
     */
    public float getLightnessWeight() {
        return mWeights[INDEX_WEIGHT_LUMA];
    }

    /**
     * Returns the weight of importance that this target places on a color's population within
     * the image.
     *
     * <p>The larger the weight, relative to the other weights, the more important that a
     * color's population being close to the most populous has on selection.</p>
     */
    public float getPopulationWeight() {
        return mWeights[INDEX_WEIGHT_POP];
    }

    /**
     * Returns whether any color selected for this target is exclusive for this target only.
     *
     * <p>If false, then the color can be selected for other targets.</p>
     */
    public boolean isExclusive() {
        return mIsExclusive;
    }

    private static void setTargetDefaultValues(final float[] values) {
        values[INDEX_MIN] = 0f;
        values[INDEX_TARGET] = 0.5f;
        values[INDEX_MAX] = 1f;
    }

    private void setDefaultWeights() {
        mWeights[INDEX_WEIGHT_SAT] = WEIGHT_SATURATION;
        mWeights[INDEX_WEIGHT_LUMA] = WEIGHT_LUMA;
        mWeights[INDEX_WEIGHT_POP] = WEIGHT_POPULATION;
    }

    void normalizeWeights() {
        float sum = 0;
        for (int i = 0, z = mWeights.length; i < z; i++) {
            float weight = mWeights[i];
            if (weight > 0) {
                sum += weight;
            }
        }
        if (sum != 0) {
            for (int i = 0, z = mWeights.length; i < z; i++) {
                if (mWeights[i] > 0) {
                    mWeights[i] /= sum;
                }
            }
        }
    }

    private static void setDefaultDarkLightnessValues(Target target) {
        target.mLightnessTargets[INDEX_TARGET] = TARGET_DARK_LUMA;
        target.mLightnessTargets[INDEX_MAX] = MAX_DARK_LUMA;
    }

    private static void setDefaultNormalLightnessValues(Target target) {
        target.mLightnessTargets[INDEX_MIN] = MIN_NORMAL_LUMA;
        target.mLightnessTargets[INDEX_TARGET] = TARGET_NORMAL_LUMA;
        target.mLightnessTargets[INDEX_MAX] = MAX_NORMAL_LUMA;
    }

    private static void setDefaultLightLightnessValues(Target target) {
        target.mLightnessTargets[INDEX_MIN] = MIN_LIGHT_LUMA;
        target.mLightnessTargets[INDEX_TARGET] = TARGET_LIGHT_LUMA;
    }

    private static void setDefaultVibrantSaturationValues(Target target) {
        target.mSaturationTargets[INDEX_MIN] = MIN_VIBRANT_SATURATION;
        target.mSaturationTargets[INDEX_TARGET] = TARGET_VIBRANT_SATURATION;
    }

    private static void setDefaultMutedSaturationValues(Target target) {
        target.mSaturationTargets[INDEX_TARGET] = TARGET_MUTED_SATURATION;
        target.mSaturationTargets[INDEX_MAX] = MAX_MUTED_SATURATION;
    }

    /**
     * Builder class for generating custom {@link Target} instances.
     */
    public final static class Builder {
        private final Target mTarget;

        /**
         * Create a new {@link Target} builder from scratch.
         */
        public Builder() {
            mTarget = new Target();
        }

        /**
         * Create a new builder based on an existing {@link Target}.
         */
        public Builder(@NonNull Target target) {
            mTarget = new Target(target);
        }

        /**
         * Set the minimum saturation value for this target.
         */
        @NonNull
        public Builder setMinimumSaturation(@FloatRange(from = 0, to = 1) float value) {
            mTarget.mSaturationTargets[INDEX_MIN] = value;
            return this;
        }

        /**
         * Set the target/ideal saturation value for this target.
         */
        @NonNull
        public Builder setTargetSaturation(@FloatRange(from = 0, to = 1) float value) {
            mTarget.mSaturationTargets[INDEX_TARGET] = value;
            return this;
        }

        /**
         * Set the maximum saturation value for this target.
         */
        @NonNull
        public Builder setMaximumSaturation(@FloatRange(from = 0, to = 1) float value) {
            mTarget.mSaturationTargets[INDEX_MAX] = value;
            return this;
        }

        /**
         * Set the minimum lightness value for this target.
         */
        @NonNull
        public Builder setMinimumLightness(@FloatRange(from = 0, to = 1) float value) {
            mTarget.mLightnessTargets[INDEX_MIN] = value;
            return this;
        }

        /**
         * Set the target/ideal lightness value for this target.
         */
        @NonNull
        public Builder setTargetLightness(@FloatRange(from = 0, to = 1) float value) {
            mTarget.mLightnessTargets[INDEX_TARGET] = value;
            return this;
        }

        /**
         * Set the maximum lightness value for this target.
         */
        @NonNull
        public Builder setMaximumLightness(@FloatRange(from = 0, to = 1) float value) {
            mTarget.mLightnessTargets[INDEX_MAX] = value;
            return this;
        }

        /**
         * Set the weight of importance that this target will place on saturation values.
         *
         * <p>The larger the weight, relative to the other weights, the more important that a color
         * being close to the target value has on selection.</p>
         *
         * <p>A weight of 0 means that it has no weight, and thus has no
         * bearing on the selection.</p>
         *
         * @see #setTargetSaturation(float)
         */
        @NonNull
        public Builder setSaturationWeight(@FloatRange(from = 0) float weight) {
            mTarget.mWeights[INDEX_WEIGHT_SAT] = weight;
            return this;
        }

        /**
         * Set the weight of importance that this target will place on lightness values.
         *
         * <p>The larger the weight, relative to the other weights, the more important that a color
         * being close to the target value has on selection.</p>
         *
         * <p>A weight of 0 means that it has no weight, and thus has no
         * bearing on the selection.</p>
         *
         * @see #setTargetLightness(float)
         */
        @NonNull
        public Builder setLightnessWeight(@FloatRange(from = 0) float weight) {
            mTarget.mWeights[INDEX_WEIGHT_LUMA] = weight;
            return this;
        }

        /**
         * Set the weight of importance that this target will place on a color's population within
         * the image.
         *
         * <p>The larger the weight, relative to the other weights, the more important that a
         * color's population being close to the most populous has on selection.</p>
         *
         * <p>A weight of 0 means that it has no weight, and thus has no
         * bearing on the selection.</p>
         */
        @NonNull
        public Builder setPopulationWeight(@FloatRange(from = 0) float weight) {
            mTarget.mWeights[INDEX_WEIGHT_POP] = weight;
            return this;
        }

        /**
         * Set whether any color selected for this target is exclusive to this target only.
         * Defaults to true.
         *
         * @param exclusive true if any the color is exclusive to this target, or false is the
         *                  color can be selected for other targets.
         */
        @NonNull
        public Builder setExclusive(boolean exclusive) {
            mTarget.mIsExclusive = exclusive;
            return this;
        }

        /**
         * Builds and returns the resulting {@link Target}.
         */
        @NonNull
        public Target build() {
            return mTarget;
        }
    }

}