All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.tommyettinger.anim8.PaletteReducer Maven / Gradle / Ivy

There is a newer version: 0.4.5
Show newest version
/*
 * Copyright (c) 2022  Tommy Ettinger
 *
 *  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 com.github.tommyettinger.anim8;

import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.*;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Random;

import static com.github.tommyettinger.anim8.ConstantData.ENCODED_AURORA;

/**
 * Data that can be used to limit the colors present in a Pixmap or other image, here with the goal of using 256 or less
 * colors in the image (for saving indexed-mode images). Can be used independently of classes like {@link AnimatedGif}
 * and {@link PNG8}, but it is meant to help with intelligently reducing the color count to fit under the maximum
 * palette size for those formats. You can use the {@link #exact(Color[])} method or its overloads to match a specific
 * palette exactly, or the {@link #analyze(Pixmap)} method or its overloads to analyze one or more Pixmaps and determine
 * which colors are most frequently-used. If using this class on its own, after calling exact(), analyze(), or a
 * constructor that uses them, you can use a specific dithering algorithm to reduce a Pixmap to the current palette.
 * Dithering algorithms that this supports:
 * 
    *
  • TOP TIER *
      *
    • {@link #reduceFloydSteinberg(Pixmap)} (Floyd-Steinberg is a very common error-diffusion dither; it's * excellent for still images and large palette sizes, but not animations. It is great for preserving shape, but * when a color isn't in the palette and it needs to try to match it, Floyd-Steinberg can leave artifacts.)
    • *
    • {@link #reduceScatter(Pixmap)} (This is Floyd-Steinberg, as above, but with some of the problem artifacts * that Floyd-Steinberg can produce perturbed by blue noise; it works well in still images and animations, but * images with gradients will do better with Neue, below. Using blue noise to edit error somewhat-randomly would * seem like it would introduce artifacts of its own, but blue noise patterns are very hard to recognize as * artificial, since they show up mostly in organic forms. Scatter holds up very well to high ditherStrength, even * to 2.0 or above, where most of the other dithers have problems, and looks similar to Floyd-Steinberg if using low * ditherStrength.)
    • *
    • {@link #reduceNeue(Pixmap)} (This is a variant on Scatter, above, that tends to be much smoother on gradients * and has very little, if any, banding when using large palettes. A quirk this has is that it doesn't usually * produce large flat areas of one color, instead preferring to dither softly between two similar colors if it can. * This tends to look similar to Floyd-Steinberg and Scatter, because it is related to both of them, but also tends * to have softer transitions between adjacent pixel colors, leading to less of a rough appearance from dithering. * Neue is the default currently because it is the only dither that both handles gradients well and preserves color * well. Blue Noise dither also handles gradients well, but doesn't always recognize color changes. Scatter handles * color well, but can have some banding. Pattern dither usually handles gradients exceptionally well, but can have * severe issues when it doesn't preserve lightness faithfully with small palettes. The list goes on. Neue can * introduce error if the palette perfectly matches the image already; in that case, use Solid.)
    • *
    • {@link #reduceBlueNoise(Pixmap)} (Uses a blue noise texture, which has almost no apparent patterns, to adjust * the amount of color correction applied to each mismatched pixel; also uses an 8x8 Bayer matrix. This adds in * a blue noise amount to each pixel, which is also what Neue does, but because this is a pure ordered dither, it * doesn't take into consideration cumulative error built up over several poorly-matched pixels. It can often fill a * whole region of color incorrectly if the palette isn't just right, though this is rare.)
    • *
    • {@link #reduceKnoll(Pixmap)} (Thomas Knoll's Pattern Dithering, used more or less verbatim; this version has * a heavy grid pattern that looks like an artifact. While the square grid here is a bit bad, it becomes very hard * to see when the palette is large enough. This reduction is the slowest here, currently, and may noticeably delay * processing on large images. Pattern dither handles complex gradients effortlessly, but struggles when the palette * size is small -- in 4-color palettes, for instance, it doesn't match lightness correctly at all.)
    • *
    *
  • *
  • OTHER TIER *
      *
    • {@link #reduceJimenez(Pixmap)} (This is a modified version of Gradient Interleaved Noise by Jorge Jimenez; * it's a kind of ordered dither that introduces a subtle wave pattern to break up solid blocks. It does well on * some animations and on smooth or rounded shapes, but still has issues with gradients. Also consider Neue for * still images and Blue Noise for animations.)
    • *
    • {@link #reduceSierraLite(Pixmap)} (Like Floyd-Steinberg, Sierra Lite is an error-diffusion dither, and it * sometimes looks better than Floyd-Steinberg, but usually is similar or worse unless the palette is small. Sierra * Lite tends to look comparable to Floyd-Steinberg if the Floyd-Steinberg dither was done with a lower * ditherStrength.If Floyd-Steinberg has unexpected artifacts, you can try Sierra Lite, and it may avoid those * issues. Using Scatter or Neue should be tried first, though.)
    • *
    • {@link #reduceChaoticNoise(Pixmap)} (Uses blue noise and pseudo-random white noise, with a carefully chosen * distribution, to disturb what would otherwise be flat bands. This does introduce chaotic or static-looking * pixels, but with larger palettes they won't be far from the original. This works fine as a last resort when you * can tolerate chaotic/fuzzy patches of poorly-defined shapes, but other dithers aren't doing well. It tends to * still have flat bands when the palette is small, and it generally looks... rather ugly.)
    • *
    • {@link #reduceKnollRoberts(Pixmap)} (This is a modified version of Thomas Knoll's Pattern Dithering; it skews * a grid-based ordered dither and also handles lightness differently from the non-Knoll dithers. It preserves shape * somewhat well, but is almost never 100% faithful to the original colors. This algorithm is rather slow; most of * the other algorithms take comparable amounts of time to each other, but KnollRoberts and especially Knoll are * sluggish.)
    • *
    • {@link #reduceSolid(Pixmap)} (No dither! Solid colors! Mostly useful when you want to preserve blocky parts * of a source image, or for some kinds of pixel/low-color art. If you have a palette that perfectly matches the * image you are dithering, then you won't need dither, and this will be the best option.)
    • *
    *
  • *
*

* Created by Tommy Ettinger on 6/23/2018. */ public class PaletteReducer { /** * DawnBringer's 256-color Aurora palette, modified slightly to fit one transparent color by removing one gray. * Aurora is available in this set of tools * for a pixel art editor, but it is usable for lots of high-color purposes. *
* These colors all have names, which can be seen previewed here. The * linked image preview also shows a nearby lighter color and two darker colors on the same sphere as the main * color; the second-lightest color is what has the listed name. The names here are used by a few other libraries, * such as colorful-gdx, but otherwise don't matter. *
* This replaced another palette, Haltonic, that wasn't hand-chosen and was much more "randomized." Aurora was the * first palette used as a default here, and it was replaced because the color metric at the time made it look bad. *
* While you can modify the individual items in this array, this is discouraged, because various constructors and * methods in this class use AURORA with a pre-made distance mapping of its colors. This mapping would become * incorrect if any colors in this array changed. */ public static final int[] AURORA = { 0x00000000, 0x010101FF, 0x131313FF, 0x252525FF, 0x373737FF, 0x494949FF, 0x5B5B5BFF, 0x6E6E6EFF, 0x808080FF, 0x929292FF, 0xA4A4A4FF, 0xB6B6B6FF, 0xC9C9C9FF, 0xDBDBDBFF, 0xEDEDEDFF, 0xFFFFFFFF, 0x007F7FFF, 0x3FBFBFFF, 0x00FFFFFF, 0xBFFFFFFF, 0x8181FFFF, 0x0000FFFF, 0x3F3FBFFF, 0x00007FFF, 0x0F0F50FF, 0x7F007FFF, 0xBF3FBFFF, 0xF500F5FF, 0xFD81FFFF, 0xFFC0CBFF, 0xFF8181FF, 0xFF0000FF, 0xBF3F3FFF, 0x7F0000FF, 0x551414FF, 0x7F3F00FF, 0xBF7F3FFF, 0xFF7F00FF, 0xFFBF81FF, 0xFFFFBFFF, 0xFFFF00FF, 0xBFBF3FFF, 0x7F7F00FF, 0x007F00FF, 0x3FBF3FFF, 0x00FF00FF, 0xAFFFAFFF, 0xBCAFC0FF, 0xCBAA89FF, 0xA6A090FF, 0x7E9494FF, 0x6E8287FF, 0x7E6E60FF, 0xA0695FFF, 0xC07872FF, 0xD08A74FF, 0xE19B7DFF, 0xEBAA8CFF, 0xF5B99BFF, 0xF6C8AFFF, 0xF5E1D2FF, 0x573B3BFF, 0x73413CFF, 0x8E5555FF, 0xAB7373FF, 0xC78F8FFF, 0xE3ABABFF, 0xF8D2DAFF, 0xE3C7ABFF, 0xC49E73FF, 0x8F7357FF, 0x73573BFF, 0x3B2D1FFF, 0x414123FF, 0x73733BFF, 0x8F8F57FF, 0xA2A255FF, 0xB5B572FF, 0xC7C78FFF, 0xDADAABFF, 0xEDEDC7FF, 0xC7E3ABFF, 0xABC78FFF, 0x8EBE55FF, 0x738F57FF, 0x587D3EFF, 0x465032FF, 0x191E0FFF, 0x235037FF, 0x3B573BFF, 0x506450FF, 0x3B7349FF, 0x578F57FF, 0x73AB73FF, 0x64C082FF, 0x8FC78FFF, 0xA2D8A2FF, 0xE1F8FAFF, 0xB4EECAFF, 0xABE3C5FF, 0x87B48EFF, 0x507D5FFF, 0x0F6946FF, 0x1E2D23FF, 0x234146FF, 0x3B7373FF, 0x64ABABFF, 0x8FC7C7FF, 0xABE3E3FF, 0xC7F1F1FF, 0xBED2F0FF, 0xABC7E3FF, 0xA8B9DCFF, 0x8FABC7FF, 0x578FC7FF, 0x57738FFF, 0x3B5773FF, 0x0F192DFF, 0x1F1F3BFF, 0x3B3B57FF, 0x494973FF, 0x57578FFF, 0x736EAAFF, 0x7676CAFF, 0x8F8FC7FF, 0xABABE3FF, 0xD0DAF8FF, 0xE3E3FFFF, 0xAB8FC7FF, 0x8F57C7FF, 0x73578FFF, 0x573B73FF, 0x3C233CFF, 0x463246FF, 0x724072FF, 0x8F578FFF, 0xAB57ABFF, 0xAB73ABFF, 0xEBACE1FF, 0xFFDCF5FF, 0xE3C7E3FF, 0xE1B9D2FF, 0xD7A0BEFF, 0xC78FB9FF, 0xC87DA0FF, 0xC35A91FF, 0x4B2837FF, 0x321623FF, 0x280A1EFF, 0x401811FF, 0x621800FF, 0xA5140AFF, 0xDA2010FF, 0xD5524AFF, 0xFF3C0AFF, 0xF55A32FF, 0xFF6262FF, 0xF6BD31FF, 0xFFA53CFF, 0xD79B0FFF, 0xDA6E0AFF, 0xB45A00FF, 0xA04B05FF, 0x5F3214FF, 0x53500AFF, 0x626200FF, 0x8C805AFF, 0xAC9400FF, 0xB1B10AFF, 0xE6D55AFF, 0xFFD510FF, 0xFFEA4AFF, 0xC8FF41FF, 0x9BF046FF, 0x96DC19FF, 0x73C805FF, 0x6AA805FF, 0x3C6E14FF, 0x283405FF, 0x204608FF, 0x0C5C0CFF, 0x149605FF, 0x0AD70AFF, 0x14E60AFF, 0x7DFF73FF, 0x4BF05AFF, 0x00C514FF, 0x05B450FF, 0x1C8C4EFF, 0x123832FF, 0x129880FF, 0x06C491FF, 0x00DE6AFF, 0x2DEBA8FF, 0x3CFEA5FF, 0x6AFFCDFF, 0x91EBFFFF, 0x55E6FFFF, 0x7DD7F0FF, 0x08DED5FF, 0x109CDEFF, 0x055A5CFF, 0x162C52FF, 0x0F377DFF, 0x004A9CFF, 0x326496FF, 0x0052F6FF, 0x186ABDFF, 0x2378DCFF, 0x699DC3FF, 0x4AA4FFFF, 0x90B0FFFF, 0x5AC5FFFF, 0xBEB9FAFF, 0x00BFFFFF, 0x007FFFFF, 0x4B7DC8FF, 0x786EF0FF, 0x4A5AFFFF, 0x6241F6FF, 0x3C3CF5FF, 0x101CDAFF, 0x0010BDFF, 0x231094FF, 0x0C2148FF, 0x5010B0FF, 0x6010D0FF, 0x8732D2FF, 0x9C41FFFF, 0x7F00FFFF, 0xBD62FFFF, 0xB991FFFF, 0xD7A5FFFF, 0xD7C3FAFF, 0xF8C6FCFF, 0xE673FFFF, 0xFF52FFFF, 0xDA20E0FF, 0xBD29FFFF, 0xBD10C5FF, 0x8C14BEFF, 0x5A187BFF, 0x641464FF, 0x410062FF, 0x320A46FF, 0x551937FF, 0xA01982FF, 0xC80078FF, 0xFF50BFFF, 0xFF6AC5FF, 0xFAA0B9FF, 0xFC3A8CFF, 0xE61E78FF, 0xBD1039FF, 0x98344DFF, 0x911437FF, }; /** * This 255-color (plus transparent) palette uses the (3,5,7) Halton sequence to get 3D points, treats those as IPT * channel values, and rejects out-of-gamut colors. This also rejects any color that is too similar to an existing * color, which in this case made this try 130958 colors before finally getting 256 that work. Using the Halton * sequence provides one of the stronger guarantees that removing any sequential items (after the first 9, which are * preset grayscale colors) will produce a similarly-distributed palette. Typically, 64 items from this are enough * to make pixel art look good enough with dithering, and it continues to improve with more colors. It has exactly 8 * colors that are purely grayscale, all right at the start after transparent. *
* Haltonic was the default palette from a fairly early version until 0.3.9, when it was replaced with Aurora. */ public static final int[] HALTONIC = new int[]{ 0x00000000, 0x010101FF, 0xFEFEFEFF, 0x7B7B7BFF, 0x555555FF, 0xAAAAAAFF, 0x333333FF, 0xE0E0E0FF, 0xC8C8C8FF, 0xBEBB4EFF, 0x1FAE9AFF, 0xC2BBA9FF, 0xB46B58FF, 0x7C82C2FF, 0xF2825BFF, 0xD55193FF, 0x8C525CFF, 0x6AEF59FF, 0x1F439BFF, 0x793210FF, 0x3B3962FF, 0x16D72EFF, 0xB53FC6FF, 0xB380C7FF, 0xEDE389FF, 0x8420C6FF, 0x291710FF, 0x69D4D3FF, 0x76121CFF, 0x1FA92AFF, 0x64852CFF, 0x7A42DBFF, 0xEA5A5EFF, 0x7E3E8CFF, 0xB8FA35FF, 0x4F15DAFF, 0xBC3E61FF, 0xA19150FF, 0x9BBD25FF, 0xF095C2FF, 0xFFC24FFF, 0x7B7CFCFF, 0x9BE8C3FF, 0xE25EC4FF, 0x3D79ADFF, 0xC0422AFF, 0x260E5DFF, 0xF645A3FF, 0xF8ACE4FF, 0xB0871FFF, 0x42582CFF, 0x549787FF, 0xE31BA2FF, 0x1E222AFF, 0xB39CF5FF, 0x8C135FFF, 0x71CB92FF, 0xB767B3FF, 0x7E5030FF, 0x406697FF, 0x502B06FF, 0xDFAC73FF, 0xC21A26FF, 0xECFE65FF, 0x7E64E4FF, 0xBFD22EFF, 0xDA938FFF, 0x8E94E8FF, 0xA0DE92FF, 0x8C6BA9FF, 0x1662FCFF, 0xCA4EECFF, 0x8899AAFF, 0x24BC57FF, 0x680AA7FF, 0xFE6885FF, 0x2E1E6EFF, 0x875695FF, 0x981C20FF, 0x47723EFF, 0xF4E54FFF, 0x71174CFF, 0xC5F8ABFF, 0x75BFC7FF, 0xF23C37FF, 0xFC73E9FF, 0x893A5FFF, 0x4F50C5FF, 0xE06635FF, 0xB00D9FFF, 0xE90FCAFF, 0x1E9CFBFF, 0x3538F9FF, 0xE3971BFF, 0x500153FF, 0x2DB2CEFF, 0xB46D86FF, 0xFE43F2FF, 0x4FF990FF, 0x434531FF, 0xE31515FF, 0xDFA24BFF, 0x4282E6FF, 0x56626FFF, 0xF8B891FF, 0x4B0932FF, 0xD769E6FF, 0x906D1DFF, 0xD51144FF, 0x76B6F8FF, 0x4DF7ECFF, 0x169355FF, 0xB7C87DFF, 0x650C83FF, 0x0AE930FF, 0xEDB71AFF, 0x78AE77FF, 0x081236FF, 0x25E5F4FF, 0x5A4382FF, 0xB1FEFAFF, 0xEA7B0BFF, 0xF372C1FF, 0xA31479FF, 0x3EDB6AFF, 0xA44210FF, 0xB2C1FAFF, 0xAE9784FF, 0xE83175FF, 0xF925DFFF, 0xAB134FFF, 0xC03E83FF, 0x117F76FF, 0xE6E21DFF, 0x6B3858FF, 0x88ED12FF, 0x3E3486FF, 0x3DBB14FF, 0xD35521FF, 0xC2836DFF, 0x244E65FF, 0xAC29F6FF, 0xE71A58FF, 0x1127ABFF, 0xD086E0FF, 0x496B1CFF, 0xD27E96FF, 0x87353AFF, 0xD308EDFF, 0x5D3BAAFF, 0x11560BFF, 0x469AC6FF, 0xEDD4B9FF, 0xA4A222FF, 0x48A75CFF, 0xBB7213FF, 0xFBBAFAFF, 0x794811FF, 0x83804EFF, 0xB1FB85FF, 0x61C56DFF, 0x9D36B1FF, 0x201693FF, 0x184BB9FF, 0x5B0606FF, 0xAB5692FF, 0x090B23FF, 0xA7593AFF, 0x14D7ADFF, 0xAC6BF1FF, 0xCC0E7EFF, 0x1B90B4FF, 0xA5A94CFF, 0x264509FF, 0xE994FDFF, 0xC1E367FF, 0x1D16D5FF, 0x1C5C7DFF, 0xCF794CFF, 0xF6FF95FF, 0x7B1A88FF, 0x68B69CFF, 0xAADAF7FF, 0x6625E1FF, 0x223308FF, 0x7147FEFF, 0xDF6A7FFF, 0xF5FE22FF, 0xB6B1D2FF, 0x35E986FF, 0x2C69D4FF, 0x6D63C8FF, 0x32042DFF, 0xF4A293FF, 0x22040DFF, 0xF2FAC2FF, 0xFFBBB2FF, 0x9D3F7CFF, 0x86694EFF, 0xD34B57FF, 0x5B2E24FF, 0xF2CF80FF, 0x10EBAFFF, 0x7B603CFF, 0xFDE5A7FF, 0xB41808FF, 0xA83F4BFF, 0xC221B4FF, 0x9604A4FF, 0x878287FF, 0x3F1C16FF, 0x5AA7FEFF, 0x55096CFF, 0x1E9922FF, 0x031050FF, 0xA284A1FF, 0x2424EDFF, 0x8FD111FF, 0x480C8BFF, 0x71FE60FF, 0xFE1D02FF, 0xFF9A60FF, 0xD44ABEFF, 0xFE7B9AFF, 0x68915EFF, 0x9EFFD1FF, 0xABAC7CFF, 0x4413BFFF, 0xF93E83FF, 0x7A9633FF, 0xA05B73FF, 0x83A3C3FF, 0x124D4AFF, 0x397E0EFF, 0x6AFEB5FF, 0x975813FF, 0xFEC704FF, 0xBC1462FF, 0xA008E0FF, 0x418886FF, 0x58CAFEFF, 0x4E7A53FF, 0x7A07FFFF, 0x8D4EBCFF, 0xFE3257FF, 0xA46BD5FF, 0xB079FFFF, 0x909478FF, 0xFC6C42FF, 0x5F3342FF, 0x6A6A9DFF, 0xFF6315FF, 0x9D56D2FF, 0x6782A7FF, 0x957F24FF, 0xD08FB9FF, }; /** * Converts an RGBA8888 int color to the RGB555 format used by {@link #OKLAB} to look up colors. * @param color an RGBA8888 int color * @return an RGB555 int color */ public static int shrink(final int color) { return (color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F); } /** * Converts an RGB555 int color to an approximation of the closest RGBA8888 color. For each 5-bit channel in * {@code color}, this gets an 8-bit value by keeping the original 5 in the most significant 5 places, then copying * the most significant 3 bits of the RGB555 color into the least significant 3 bits of the 8-bit value. In * practice, this means the lowest 5-bit value produces the lowest 8-bit value (00000 to 00000000), and the highest * 5-bit value produces the highest 8-bit value (11111 to 11111111). This always assigns a fully-opaque value to * alpha (255, or 0xFF). * @param color an RGB555 color * @return an approximation of the closest RGBA8888 color; alpha is always fully opaque */ public static int stretch(final int color) { return (color << 17 & 0xF8000000) | (color << 12 & 0x07000000) | (color << 14 & 0xF80000) | (color << 9 & 0x070000) | (color << 11 & 0xF800) | (color << 6 & 0x0700) | 0xFF; } /** * Changes the curve of a requested L value so that it matches the internally-used curve. This takes a curve with a * very-dark area similar to sRGB (a very small one), and makes it significantly larger. This is typically used on * "to Oklab" conversions. * @param L lightness, from 0 to 1 inclusive * @return an adjusted L value that can be used internally */ public static float forwardLight(final float L) { final float shape = 0.64516133f, turning = 0.95f; final float d = turning - L; float r; if(d < 0) r = ((1f - turning) * (L - 1f)) / (1f - (L + shape * d)) + 1f; else r = (turning * L) / (1e-20f + (L + shape * d)); return r * r; } // public static float forwardLight(final float L) { // return (L - 1.004f) / (1f - L * 0.4285714f) + 1.004f; // } /** * Changes the curve of the internally-used lightness when it is output to another format. This makes the very-dark * area smaller, matching (kind-of) the curve that the standard sRGB lightness uses. This is typically used on "from * Oklab" conversions. * @param L lightness, from 0 to 1 inclusive * @return an adjusted L value that can be fed into a conversion to RGBA or something similar */ public static float reverseLight(float L) { L = (float) Math.sqrt(L); final float shape = 1.55f, turning = 0.95f; final float d = turning - L; float r; if(d < 0) r = ((1f - turning) * (L - 1f)) / (1f - (L + shape * d)) + 1f; else r = (turning * L) / (1e-20f + (L + shape * d)); return r; } // public static float reverseLight(final float L) { // return (L - 0.993f) / (1f + L * 0.75f) + 0.993f; // } /** * Stores Oklab components corresponding to RGB555 indices. * OKLAB[0] stores L (lightness) from 0.0 to 1.0 . * OKLAB[1] stores A, which is something like a green-magenta axis, from -0.5 (green) to 0.5 (red). * OKLAB[2] stores B, which is something like a blue-orange axis, from -0.5 (blue) to 0.5 (yellow). * OKLAB[3] stores the hue in radians from -PI to PI, with red at 0, yellow at PI/2, and blue at -PI/2. *
* The indices into each of these float[] values store red in bits 10-14, green in bits 5-9, and blue in bits 0-4. * It's ideal to work with these indices with bitwise operations, as with {@code (r << 10 | g << 5 | b)}, where r, * g, and b are all in the 0-31 range inclusive. It's usually easiest to convert an RGBA8888 int color to an RGB555 * color with {@link #shrink(int)}. */ public static final float[][] OKLAB = new float[4][0x8000]; /** * A 4096-element byte array as a 64x64 grid of bytes. When arranged into a grid, the bytes will follow a blue noise * frequency (in this case, they will have a triangular distribution for its bytes, so values near 0 are much more * common). This is used inside this library to create {@link #TRI_BLUE_NOISE_MULTIPLIERS}, which is used in * {@link #reduceScatter(Pixmap)}. It is also used directly by {@link #reduceBlueNoise(Pixmap)}, * {@link #reduceNeue(Pixmap)}, and {@link #reduceChaoticNoise(Pixmap)}. *
* While, for some reason, you could change the contents to some other distribution of bytes, I don't know why this * would be needed. */ public static final byte[] TRI_BLUE_NOISE = ConstantData.TRI_BLUE_NOISE; /** * A 4096-element byte array as a 64x64 grid of bytes. When arranged into a grid, the bytes will follow a blue noise * frequency (in this case, they will have a triangular distribution for its bytes, so values near 0 are much more * common). This is used inside this library by {@link #reduceBlueNoise(Pixmap)}. *
* While, for some reason, you could change the contents to some other distribution of bytes, I don't know why this * would be needed. */ public static final byte[] TRI_BLUE_NOISE_B = ConstantData.TRI_BLUE_NOISE_B; /** * A 4096-element byte array as a 64x64 grid of bytes. When arranged into a grid, the bytes will follow a blue noise * frequency (in this case, they will have a triangular distribution for its bytes, so values near 0 are much more * common). This is used inside this library by {@link #reduceBlueNoise(Pixmap)}. *
* While, for some reason, you could change the contents to some other distribution of bytes, I don't know why this * would be needed. */ public static final byte[] TRI_BLUE_NOISE_C = ConstantData.TRI_BLUE_NOISE_C; /** * A 64x64 grid of floats, with a median value of about 1.0, generated using the triangular-distributed blue noise * from {@link #TRI_BLUE_NOISE}. If you randomly selected two floats from this and multiplied them, the average * result should be 1.0; half of the items in this should be between 1 and {@link Math#E}, and the other half should * be the inverses of the first half (between {@code 1.0/Math.E} and 1). *
* While, for some reason, you could change the contents to some other distribution of bytes, I don't know why this * would be needed. */ public static final float[] TRI_BLUE_NOISE_MULTIPLIERS = ConstantData.TRI_BLUE_NOISE_MULTIPLIERS; static { float rf, gf, bf, lf, mf, sf; int idx = 0; for (int ri = 0; ri < 32; ri++) { rf = (float) (ri * ri * 0.0010405827263267429); // 1.0 / 31.0 / 31.0 for (int gi = 0; gi < 32; gi++) { gf = (float) (gi * gi * 0.0010405827263267429); // 1.0 / 31.0 / 31.0 for (int bi = 0; bi < 32; bi++) { bf = (float) (bi * bi * 0.0010405827263267429); // 1.0 / 31.0 / 31.0 lf = OtherMath.cbrt(0.4121656120f * rf + 0.5362752080f * gf + 0.0514575653f * bf); mf = OtherMath.cbrt(0.2118591070f * rf + 0.6807189584f * gf + 0.1074065790f * bf); sf = OtherMath.cbrt(0.0883097947f * rf + 0.2818474174f * gf + 0.6302613616f * bf); OKLAB[0][idx] = forwardLight( 0.2104542553f * lf + 0.7936177850f * mf - 0.0040720468f * sf); OKLAB[1][idx] = 1.9779984951f * lf - 2.4285922050f * mf + 0.4505937099f * sf; OKLAB[2][idx] = 0.0259040371f * lf + 0.7827717662f * mf - 0.8086757660f * sf; OKLAB[3][idx] = OtherMath.atan2(OKLAB[2][idx], OKLAB[1][idx]); idx++; } } } // for (int i = 1; i < 256; i++) { // EXACT_LOOKUP[i] = OtherMath.barronSpline(i / 255f, 4f, 0.5f); // ANALYTIC_LOOKUP[i] = OtherMath.barronSpline(i / 255f, 3f, 0.5f); // } // // double r, g, b, x, y, z; // int idx = 0; // for (int ri = 0; ri < 32; ri++) { // r = ri / 31.0; // r = ((r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92); // for (int gi = 0; gi < 32; gi++) { // g = gi / 31.0; // g = ((g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92); // for (int bi = 0; bi < 32; bi++) { // b = bi / 31.0; // b = ((b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92); // // x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.950489; // 0.96422; // y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.000000; // 1.00000; // z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.088840; // 0.82521; // // x = (x > 0.008856) ? Math.cbrt(x) : (7.787037037037037 * x) + 0.13793103448275862; // y = (y > 0.008856) ? Math.cbrt(y) : (7.787037037037037 * y) + 0.13793103448275862; // z = (z > 0.008856) ? Math.cbrt(z) : (7.787037037037037 * z) + 0.13793103448275862; // // LAB[0][idx] = (116.0 * y) - 16.0; // LAB[1][idx] = 500.0 * (x - y); // LAB[2][idx] = 200.0 * (y - z); // idx++; // } // } // } } public static int oklabToRGB(float L, float A, float B, float alpha) { L = reverseLight(L); float l = (L + 0.3963377774f * A + 0.2158037573f * B); float m = (L - 0.1055613458f * A - 0.0638541728f * B); float s = (L - 0.0894841775f * A - 1.2914855480f * B); l *= l * l; m *= m * m; s *= s * s; final int r = (int)(Math.sqrt(Math.min(Math.max(+4.0767245293f * l - 3.3072168827f * m + 0.2307590544f * s, 0.0f), 1.0f)) * 255.999f); final int g = (int)(Math.sqrt(Math.min(Math.max(-1.2681437731f * l + 2.6093323231f * m - 0.3411344290f * s, 0.0f), 1.0f)) * 255.999f); final int b = (int)(Math.sqrt(Math.min(Math.max(-0.0041119885f * l - 0.7034763098f * m + 1.7068625689f * s, 0.0f), 1.0f)) * 255.999f); return r << 24 | g << 16 | b << 8 | (int)(alpha * 255.999f); } /** * Stores the byte indices into {@link #paletteArray} (when treated as unsigned; mask with 255) corresponding to * RGB555 colors (you can get an RGB555 int from an RGBA8888 int using {@link #shrink(int)}). This is not especially * likely to be useful externally except to make a preload code for later usage. If you have a way to write and read * bytes from a file, you can calculate a frequently-used palette once using {@link #exact(int[])} or * {@link #analyze(Pixmap)}, write this field to file, and on later runs you can load the 32768-element byte array * to speed up construction using {@link #PaletteReducer(int[], byte[])}. Editing this field is strongly * discouraged; use {@link #exact(int[])} or {@link #analyze(Pixmap)} to set the palette as a whole. */ public final byte[] paletteMapping = new byte[0x8000]; /** * The RGBA8888 int colors this can reduce an image to use. This is public, and since it is an array you can modify * its contents, but you should only change this if you know what you are doing. It is closely related to the * contents of the {@link #paletteMapping} field, and paletteMapping should typically be changed by * {@link #exact(int[])}, {@link #analyze(Pixmap)}, or {@link #loadPreloadFile(FileHandle)}. Because paletteMapping * only contains indices into this paletteArray, if paletteArray changes then the closest-color consideration may be * altered. This field can be safely altered, usually, by {@link #alterColorsLightness(Interpolation)} or * {@link #alterColorsOklab(Interpolation, Interpolation, Interpolation)}. */ public final int[] paletteArray = new int[256]; FloatArray curErrorRedFloats, nextErrorRedFloats, curErrorGreenFloats, nextErrorGreenFloats, curErrorBlueFloats, nextErrorBlueFloats; /** * How many colors are in the palette here; this is at most 256, and typically includes one fully-transparent color. */ public int colorCount; public IntIntMap reverseMap; /** * Determines how strongly to apply noise or other effects during dithering. This is usually half the value set with * {@link #setDitherStrength(float)}. */ protected float ditherStrength = 1f; /** * Typically between 0.5 and 1, this should get closer to 1 with larger palette sizes, and closer to 0.5 with * smaller palettes. Within anim8-gdx, this is generally calculated with {@code Math.exp(-1.375 / colorCount)}. */ protected float populationBias = 0.5f; /** * If this PaletteReducer has already calculated a palette, you can use this to save the slightly-slow-to-compute * palette mapping in a preload file for later runs. Once you have the file and the same int array originally used * for the RGBA8888 colors (e.g. {@code intColors}), you can load it when constructing a * PaletteReducer with {@code new PaletteReducer(intColors, PaletteReducer.loadPreloadFile(theFile))}. * @param file a writable non-null FileHandle; this will overwrite a file already present if it has the same name */ public void writePreloadFile(FileHandle file){ file.writeBytes(paletteMapping, false); } /** * If you saved a preload file with {@link #writePreloadFile(FileHandle)}, you can load it and give it to a * constructor with: {@code new PaletteReducer(intColors, PaletteReducer.loadPreloadFile(theFile))}, where intColors * is the original int array of RGBA8888 colors and theFile is the preload file written previously. * @param file a readable non-null FileHandle that should have been written by * {@link #writePreloadFile(FileHandle)}, or otherwise contain the bytes of {@link #paletteMapping} * @return a byte array that should have a length of exactly 32768, to be passed to {@link #PaletteReducer(int[], byte[])} */ public static byte[] loadPreloadFile(FileHandle file) { return file.readBytes(); } /** * Constructs a default PaletteReducer that uses the "Aurora" 255-color-plus-transparent palette. * Note that this uses a more-detailed and higher-quality metric than you would get by just specifying * {@code new PaletteReducer(PaletteReducer.AURORA)}; this metric would be too slow to calculate at * runtime, but as pre-calculated data it works very well. */ public PaletteReducer() { exact(AURORA, ENCODED_AURORA); } /** * Constructs a PaletteReducer that uses the given array of RGBA8888 ints as a palette (see {@link #exact(int[])} * for more info). * * @param rgbaPalette an array of RGBA8888 ints to use as a palette */ public PaletteReducer(int[] rgbaPalette) { if(rgbaPalette == null) { exact(AURORA, ENCODED_AURORA); return; } exact(rgbaPalette); } /** * Constructs a PaletteReducer that uses the given array of RGBA8888 ints as a palette (see * {@link #exact(int[], int)} for more info). * * @param rgbaPalette an array of RGBA8888 ints to use as a palette * @param limit how many int items to use from rgbaPalette (this always starts at index 0) */ public PaletteReducer(int[] rgbaPalette, int limit) { if(rgbaPalette == null) { exact(AURORA, ENCODED_AURORA); return; } exact(rgbaPalette, limit); } /** * Constructs a PaletteReducer that uses the given array of Color objects as a palette (see {@link #exact(Color[])} * for more info). * * @param colorPalette an array of Color objects to use as a palette */ public PaletteReducer(Color[] colorPalette) { if(colorPalette == null) { exact(AURORA, ENCODED_AURORA); return; } exact(colorPalette); } /** * Constructs a PaletteReducer that uses the given array of Color objects as a palette (see * {@link #exact(Color[], int)} for more info). * * @param colorPalette an array of Color objects to use as a palette */ public PaletteReducer(Color[] colorPalette, int limit) { if(colorPalette == null) { exact(AURORA, ENCODED_AURORA); return; } exact(colorPalette, limit); } /** * Constructs a PaletteReducer that analyzes the given Pixmap for color count and frequency to generate a palette * (see {@link #analyze(Pixmap)} for more info). * * @param pixmap a Pixmap to analyze in detail to produce a palette */ public PaletteReducer(Pixmap pixmap) { if(pixmap == null) { exact(AURORA, ENCODED_AURORA); return; } analyze(pixmap); } /** * Constructs a PaletteReducer that analyzes the given Pixmaps for color count and frequency to generate a palette * (see {@link #analyze(Array)} for more info). * * @param pixmaps an Array of Pixmap to analyze in detail to produce a palette */ public PaletteReducer(Array pixmaps) { if(pixmaps == null) { exact(AURORA, ENCODED_AURORA); return; } analyze(pixmaps); } /** * Constructs a PaletteReducer that uses the given array of RGBA8888 ints as a palette (see * {@link #exact(int[], byte[])} for more info) and an encoded byte array to use to look up pre-loaded color data. * You can use {@link #writePreloadFile(FileHandle)} to write the preload data for a given PaletteReducer, and * {@link #loadPreloadFile(FileHandle)} to get a byte array of preload data from a previously-written file. * @param palette an array of RGBA8888 ints to use as a palette * @param preload a byte array containing preload data */ public PaletteReducer(int[] palette, byte[] preload) { exact(palette, preload); } /** * Constructs a PaletteReducer that analyzes the given Pixmap for color count and frequency to generate a palette * (see {@link #analyze(Pixmap, double)} for more info). * * @param pixmap a Pixmap to analyze in detail to produce a palette * @param threshold the minimum difference between colors required to put them in the palette (default 100) */ public PaletteReducer(Pixmap pixmap, double threshold) { analyze(pixmap, threshold); } // return (RGB_POWERS[Math.abs(r1 - r2)] // + RGB_POWERS[256+Math.abs(g1 - g2)] // + RGB_POWERS[512+Math.abs(b1 - b2)]) * 0x1p-10; // public static double difference(int color1, int color2) { // if (((color1 ^ color2) & 0x80) == 0x80) return Double.POSITIVE_INFINITY; // final int indexA = (color1 >>> 17 & 0x7C00) | (color1 >>> 14 & 0x3E0) | (color1 >>> 11 & 0x1F), // indexB = (color2 >>> 17 & 0x7C00) | (color2 >>> 14 & 0x3E0) | (color2 >>> 11 & 0x1F); // float // L = OKLAB[0][indexA] - OKLAB[0][indexB], // A = OKLAB[1][indexA] - OKLAB[1][indexB], // B = OKLAB[2][indexA] - OKLAB[2][indexB]; // L *= L; // A *= A; // B *= B; // return (L * L + A * A + B * B) * 0x1p+27; // } // public static double difference(int color1, int r2, int g2, int b2) { // if ((color1 & 0x80) == 0) return Double.POSITIVE_INFINITY; // final int indexA = (color1 >>> 17 & 0x7C00) | (color1 >>> 14 & 0x3E0) | (color1 >>> 11 & 0x1F), // indexB = (r2 << 7 & 0x7C00) | (g2 << 2 & 0x3E0) | (b2 >>> 3); // float // L = OKLAB[0][indexA] - OKLAB[0][indexB], // A = OKLAB[1][indexA] - OKLAB[1][indexB], // B = OKLAB[2][indexA] - OKLAB[2][indexB]; // L *= L; // A *= A; // B *= B; // return (L * L + A * A + B * B) * 0x1p+27; // } // public static double difference(int r1, int g1, int b1, int r2, int g2, int b2) { // int indexA = (r1 << 7 & 0x7C00) | (g1 << 2 & 0x3E0) | (b1 >>> 3), // indexB = (r2 << 7 & 0x7C00) | (g2 << 2 & 0x3E0) | (b2 >>> 3); // float // L = OKLAB[0][indexA] - OKLAB[0][indexB], // A = OKLAB[1][indexA] - OKLAB[1][indexB], // B = OKLAB[2][indexA] - OKLAB[2][indexB]; // L *= L; // A *= A; // B *= B; // return (L * L + A * A + B * B) * 0x1p+27; // } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. * If you want to change this, just change {@link #differenceMatch(int, int, int, int, int, int)}, which this * calls. * @param color1 the first color, as an RGBA8888 int * @param color2 the second color, as an RGBA8888 int * @return the squared Euclidean distance between colors 1 and 2 */ public double differenceMatch(int color1, int color2) { if (((color1 ^ color2) & 0x80) == 0x80) return Double.MAX_VALUE; return differenceMatch(color1 >>> 24, color1 >>> 16 & 0xFF, color1 >>> 8 & 0xFF, color2 >>> 24, color2 >>> 16 & 0xFF, color2 >>> 8 & 0xFF); } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. * If you want to change this, just change {@link #differenceAnalyzing(int, int, int, int, int, int)}, which this * calls. * @param color1 the first color, as an RGBA8888 int * @param color2 the second color, as an RGBA8888 int * @return the squared Euclidean distance between colors 1 and 2 */ public double differenceAnalyzing(int color1, int color2) { if (((color1 ^ color2) & 0x80) == 0x80) return Double.MAX_VALUE; return differenceAnalyzing(color1 >>> 24, color1 >>> 16 & 0xFF, color1 >>> 8 & 0xFF, color2 >>> 24, color2 >>> 16 & 0xFF, color2 >>> 8 & 0xFF); } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. * If you want to change this, just change {@link #differenceHW(int, int, int, int, int, int)}, which this calls. * @param color1 the first color, as an RGBA8888 int * @param color2 the second color, as an RGBA8888 int * @return the squared Euclidean distance between colors 1 and 2 */ public double differenceHW(int color1, int color2) { if (((color1 ^ color2) & 0x80) == 0x80) return Double.MAX_VALUE; return differenceHW(color1 >>> 24, color1 >>> 16 & 0xFF, color1 >>> 8 & 0xFF, color2 >>> 24, color2 >>> 16 & 0xFF, color2 >>> 8 & 0xFF); } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. * If you want to change this, just change {@link #differenceMatch(int, int, int, int, int, int)}, which this calls. * @param color1 the first color, as an RGBA8888 int * @param r2 red of the second color, from 0 to 255 * @param g2 green of the second color, from 0 to 255 * @param b2 blue of the second color, from 0 to 255 * @return the squared Euclidean distance between colors 1 and 2 */ public double differenceMatch(int color1, int r2, int g2, int b2) { if((color1 & 0x80) == 0) return Double.MAX_VALUE; return differenceMatch(color1 >>> 24, color1 >>> 16 & 0xFF, color1 >>> 8 & 0xFF, r2, g2, b2); } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. If * you want to change this, just change {@link #differenceAnalyzing(int, int, int, int, int, int)}, which this * calls. * @param color1 the first color, as an RGBA8888 int * @param r2 red of the second color, from 0 to 255 * @param g2 green of the second color, from 0 to 255 * @param b2 blue of the second color, from 0 to 255 * @return the squared Euclidean distance between colors 1 and 2 */ public double differenceAnalyzing(int color1, int r2, int g2, int b2) { if((color1 & 0x80) == 0) return Double.MAX_VALUE; return differenceAnalyzing(color1 >>> 24, color1 >>> 16 & 0xFF, color1 >>> 8 & 0xFF, r2, g2, b2); } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. If * you want to change this, just change {@link #differenceHW(int, int, int, int, int, int)}, which this calls. * @param color1 the first color, as an RGBA8888 int * @param r2 red of the second color, from 0 to 255 * @param g2 green of the second color, from 0 to 255 * @param b2 blue of the second color, from 0 to 255 * @return the squared Euclidean distance between colors 1 and 2 */ public double differenceHW(int color1, int r2, int g2, int b2) { if((color1 & 0x80) == 0) return Double.MAX_VALUE; return differenceHW(color1 >>> 24, color1 >>> 16 & 0xFF, color1 >>> 8 & 0xFF, r2, g2, b2); } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. * This can be changed in an extending (possibly anonymous) class to use a different squared metric. This is used * when matching to an existing palette, as with {@link #exact(int[])}. *
* This uses Euclidean distance between the RGB colors in the 256-edge-length color cube. This does absolutely * nothing fancy with the colors, but this approach does well often. The same code is used by * {@link #differenceMatch(int, int, int, int, int, int)}, * {@link #differenceAnalyzing(int, int, int, int, int, int)}, and * {@link #differenceHW(int, int, int, int, int, int)}, but classes can (potentially anonymously) subclass * PaletteReducer to change one, some, or all of these methods. The other difference methods call the 6-argument * overloads, so the override only needs to affect one method. * * @param r1 red of the first color, from 0 to 255 * @param g1 green of the first color, from 0 to 255 * @param b1 blue of the first color, from 0 to 255 * @param r2 red of the second color, from 0 to 255 * @param g2 green of the second color, from 0 to 255 * @param b2 blue of the second color, from 0 to 255 * @return the squared Euclidean distance between colors 1 and 2 */ public double differenceMatch(int r1, int g1, int b1, int r2, int g2, int b2) { int rf = (r1 - r2); int gf = (g1 - g2); int bf = (b1 - b2); return (rf * rf + gf * gf + bf * bf); // double rf = (EXACT_LOOKUP[r1] - EXACT_LOOKUP[r2]) * 1.55;// rf *= rf;// * 0.875; // double gf = (EXACT_LOOKUP[g1] - EXACT_LOOKUP[g2]) * 2.05;// gf *= gf;// * 0.75; // double bf = (EXACT_LOOKUP[b1] - EXACT_LOOKUP[b2]) * 0.90;// bf *= bf;// * 1.375; // // return (rf * rf + gf * gf + bf * bf) * 0x1.8p17; } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. * This can be changed in an extending (possibly anonymous) class to use a different squared metric. This is used * when analyzing an image, as with {@link #analyze(Pixmap)}. *
* This uses Euclidean distance between the RGB colors in the 256-edge-length color cube. This does absolutely * nothing fancy with the colors, but this approach does well often. The same code is used by * {@link #differenceMatch(int, int, int, int, int, int)}, * {@link #differenceAnalyzing(int, int, int, int, int, int)}, and * {@link #differenceHW(int, int, int, int, int, int)}, but classes can (potentially anonymously) subclass * PaletteReducer to change one, some, or all of these methods. The other difference methods call the 6-argument * overloads, so the override only needs to affect one method. * * @param r1 red of the first color, from 0 to 255 * @param g1 green of the first color, from 0 to 255 * @param b1 blue of the first color, from 0 to 255 * @param r2 red of the second color, from 0 to 255 * @param g2 green of the second color, from 0 to 255 * @param b2 blue of the second color, from 0 to 255 * @return the squared Euclidean distance between colors 1 and 2 */ public double differenceAnalyzing(int r1, int g1, int b1, int r2, int g2, int b2) { int rf = (r1 - r2); int gf = (g1 - g2); int bf = (b1 - b2); return (rf * rf + gf * gf + bf * bf); // double rf = (ANALYTIC_LOOKUP[r1] - ANALYTIC_LOOKUP[r2]); // double gf = (ANALYTIC_LOOKUP[g1] - ANALYTIC_LOOKUP[g2]); // double bf = (ANALYTIC_LOOKUP[b1] - ANALYTIC_LOOKUP[b2]); // // return (rf * rf + gf * gf + bf * bf) * 0x1.4p17; } /** * Gets a squared estimate of how different two colors are, with noticeable differences typically at least 25. * This can be changed in an extending (possibly anonymous) class to use a different squared metric. This is used * when analyzing an image with {@link #analyzeHueWise(Pixmap, double, int)} . *
* This uses Euclidean distance between the RGB colors in the 256-edge-length color cube. This does absolutely * nothing fancy with the colors, but this approach does well often. The same code is used by * {@link #differenceMatch(int, int, int, int, int, int)}, * {@link #differenceAnalyzing(int, int, int, int, int, int)}, and * {@link #differenceHW(int, int, int, int, int, int)}, but classes can (potentially anonymously) subclass * PaletteReducer to change one, some, or all of these methods. The other difference methods call the 6-argument * overloads, so the override only needs to affect one method. * * @param r1 red of the first color, from 0 to 255 * @param g1 green of the first color, from 0 to 255 * @param b1 blue of the first color, from 0 to 255 * @param r2 red of the second color, from 0 to 255 * @param g2 green of the second color, from 0 to 255 * @param b2 blue of the second color, from 0 to 255 * @return the squared Euclidean distance, between colors 1 and 2 */ public double differenceHW(int r1, int g1, int b1, int r2, int g2, int b2) { int rf = (r1 - r2); int gf = (g1 - g2); int bf = (b1 - b2); return (rf * rf + gf * gf + bf * bf); // double rf = (ANALYTIC_LOOKUP[r1] - ANALYTIC_LOOKUP[r2]); // double gf = (ANALYTIC_LOOKUP[g1] - ANALYTIC_LOOKUP[g2]); // double bf = (ANALYTIC_LOOKUP[b1] - ANALYTIC_LOOKUP[b2]); // return (rf * rf + gf * gf + bf * bf) * 0x1.4p17; } /** * Resets the palette to the 256-color (including transparent) "Aurora" palette. PaletteReducer already * stores most of the calculated data needed to use this one palette. Note that this uses a more-detailed * and higher-quality metric than you would get by just specifying * {@code new PaletteReducer(PaletteReducer.AURORA)}; this metric would be too slow to calculate at * runtime, but as pre-calculated data it works very well. */ public void setDefaultPalette(){ exact(AURORA, ENCODED_AURORA); } /** * Builds the palette information this PNG8 stores from the RGBA8888 ints in {@code rgbaPalette}, up to 256 colors. * Alpha is not preserved except for the first item in rgbaPalette, and only if it is {@code 0} (fully transparent * black); otherwise all items are treated as opaque. If rgbaPalette is null, empty, or only has one color, then * this defaults to the "Aurora" palette with 256 well-distributed colors (including transparent). * * @param rgbaPalette an array of RGBA8888 ints; all will be used up to 256 items or the length of the array */ public void exact(int[] rgbaPalette) { exact(rgbaPalette, 256); } /** * Builds the palette information this PNG8 stores from the RGBA8888 ints in {@code rgbaPalette}, up to 256 colors * or {@code limit}, whichever is less. * Alpha is not preserved except for the first item in rgbaPalette, and only if it is {@code 0} (fully transparent * black); otherwise all items are treated as opaque. If rgbaPalette is null, empty, or only has one color, or if * limit is less than 2, then this defaults to the "Aurora" palette with 256 well-distributed colors (including * transparent). * * @param rgbaPalette an array of RGBA8888 ints; all will be used up to 256 items or the length of the array * @param limit a limit on how many int items to use from rgbaPalette; useful if rgbaPalette is from an IntArray */ public void exact(int[] rgbaPalette, int limit) { if (rgbaPalette == null || rgbaPalette.length < 2 || limit < 2) { exact(AURORA, ENCODED_AURORA); return; } Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); final int plen = Math.min(Math.min(256, limit), rgbaPalette.length); colorCount = plen; populationBias = (float) Math.exp(-1.125/colorCount); int color, c2; double dist; if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < plen; i++) { color = rgbaPalette[i]; if ((color & 0x80) != 0) { paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; reverseMap.put(color, i); } else reverseMap.put(0, i); } int rr, gg, bb; for (int r = 0; r < 32; r++) { rr = (r << 3 | r >>> 2); for (int g = 0; g < 32; g++) { gg = (g << 3 | g >>> 2); for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (paletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = 1E100; for (int i = 1; i < plen; i++) { if (dist > (dist = Math.min(dist, differenceMatch(paletteArray[i], rr, gg, bb)))) paletteMapping[c2] = (byte) i; } } } } } } /** * Builds the palette information this PaletteReducer stores from the given array of RGBA8888 ints as a palette (see * {@link #exact(int[])} for more info) and an encoded byte array to use to look up pre-loaded color data. The * encoded byte array can be copied out of the {@link #paletteMapping} of an existing PaletteReducer. There's * slightly more startup time spent when initially calling {@link #exact(int[])}, but it will produce the same * result. You can store the paletteMapping from that PaletteReducer once, however you want to store it, and send it * back to this on later runs. * * @param palette an array of RGBA8888 ints to use as a palette * @param preload a byte array with exactly 32768 (or 0x8000) items, containing {@link #paletteMapping} data */ public void exact(int[] palette, byte[] preload) { if(palette == null || preload == null) { System.arraycopy(AURORA, 0, paletteArray, 0, 256); System.arraycopy(ENCODED_AURORA, 0, paletteMapping, 0, 0x8000); colorCount = 256; populationBias = (float) Math.exp(-1.125 / 256.0); if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } return; } colorCount = Math.min(256, palette.length); System.arraycopy(palette, 0, paletteArray, 0, colorCount); System.arraycopy(preload, 0, paletteMapping, 0, 0x8000); if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } populationBias = (float) Math.exp(-1.125/colorCount); } /** * Builds the palette information this PaletteReducer stores from the Color objects in {@code colorPalette}, up to * 256 colors. * Alpha is not preserved except for the first item in colorPalette, and only if its r, g, b, and a values are all * 0f (fully transparent black); otherwise all items are treated as opaque. If rgbaPalette is null, empty, or only * has one color, then this defaults to the "Aurora" palette with 256 well-distributed colors (including * transparent). * * @param colorPalette an array of Color objects; all will be used up to 256 items or the length of the array */ public void exact(Color[] colorPalette) { exact(colorPalette, 256); } /** * Builds the palette information this PaletteReducer stores from the Color objects in {@code colorPalette}, up to * 256 colors or {@code limit}, whichever is less. * Alpha is not preserved except for the first item in colorPalette, and only if its r, g, b, and a values are all * 0f (fully transparent black); otherwise all items are treated as opaque. If rgbaPalette is null, empty, only has * one color, or limit is less than 2, then this defaults to the "Aurora" palette with 256 well-distributed * colors (including transparent). * * @param colorPalette an array of Color objects; all will be used up to 256 items, limit, or the length of the array * @param limit a limit on how many Color items to use from colorPalette; useful if colorPalette is from an Array */ public void exact(Color[] colorPalette, int limit) { if (colorPalette == null || colorPalette.length < 2 || limit < 2) { exact(AURORA, ENCODED_AURORA); return; } Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); final int plen = Math.min(Math.min(256, colorPalette.length), limit); colorCount = plen; populationBias = (float) Math.exp(-1.125/colorCount); int color, c2; double dist; if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < plen; i++) { color = Color.rgba8888(colorPalette[i]); paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; reverseMap.put(color, i); } int rr, gg, bb; for (int r = 0; r < 32; r++) { rr = (r << 3 | r >>> 2); for (int g = 0; g < 32; g++) { gg = (g << 3 | g >>> 2); for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (paletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = 0x7FFFFFFF; for (int i = 1; i < plen; i++) { if (dist > (dist = Math.min(dist, differenceMatch(paletteArray[i], rr, gg, bb)))) paletteMapping[c2] = (byte) i; } } } } } } /** * Analyzes {@code pixmap} for color count and frequency, building a palette with at most 256 colors if there are * too many colors to store in a PNG-8 palette. If there are 256 or less colors, this uses the exact colors * (although with at most one transparent color, and no alpha for other colors); this will always reserve a palette * entry for transparent (even if the image has no transparency) because it uses palette index 0 in its analysis * step. Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will dither colors that * aren't exact, and dithering works better when the palette can choose colors that are sufficiently different, this * uses a threshold value to determine whether it should permit a less-common color into the palette, and if the * second color is different enough (as measured by {@link #differenceAnalyzing(int, int)}) by a value of at least * 100, it is * allowed in the palette, otherwise it is kept out for being too similar to existing colors. This doesn't return a * value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} field or can be used directly to {@link #reduce(Pixmap)} a Pixmap. * * @param pixmap a Pixmap to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)} or by PNG8 */ public void analyze(Pixmap pixmap) { analyze(pixmap, 100); } protected static final Comparator entryComparator = new Comparator() { @Override public int compare(IntIntMap.Entry o1, IntIntMap.Entry o2) { return o2.value - o1.value; } }; // private static final Comparator intFloatEntryComparator = new Comparator() { // @Override // public int compare(IntFloatMap.Entry o1, IntFloatMap.Entry o2) { // return NumberUtils.floatToIntBits(o2.value - o1.value); // } // }; /** * Just like Comparator, but compares primitive ints. */ public interface IntComparator { /** * Compares its two primitive-type arguments for order. Returns a negative * integer, zero, or a positive integer as the first argument is less than, * equal to, or greater than the second. * * @return a negative integer, zero, or a positive integer as the first argument * is less than, equal to, or greater than the second. * @see Comparator */ int compare(int k1, int k2); } /** * Compares shrunken indices (RGB555) by lightness as Oklab knows it. */ protected static final IntComparator lightnessComparator = new IntComparator() { @Override public int compare(int k1, int k2) { return NumberUtils.floatToIntBits(OKLAB[0][k2] - OKLAB[0][k1]); } }; /** * Compares shrunken indices (RGB555) by hue as Oklab knows it. */ protected static final IntComparator hueComparator = new IntComparator() { @Override public int compare(int k1, int k2) { return NumberUtils.floatToIntBits(OKLAB[3][k2] - OKLAB[3][k1]); } }; /// Start of code primarily based on FastUtil. /// See https://github.com/vigna/fastutil/blob/8.5.8/LICENSE-2.0 for // the license of this block (Apache License 2.0). private static void swap (int[] items, int first, int second) { int firstValue = items[first]; items[first] = items[second]; items[second] = firstValue; } /** * Transforms two consecutive sorted ranges into a single sorted range. The initial ranges are * {@code [first..middle)} and {@code [middle..last)}, and the resulting range is * {@code [first..last)}. Elements in the first input range will precede equal elements in * the second. */ private static void inPlaceMerge (int[] items, final int from, int mid, final int to, final IntComparator comp) { if (from >= mid || mid >= to) {return;} if (to - from == 2) { if (comp.compare(items[mid], items[from]) < 0) {swap(items, from, mid);} return; } int firstCut; int secondCut; if (mid - from > to - mid) { firstCut = from + (mid - from) / 2; secondCut = lowerBound(items, mid, to, firstCut, comp); } else { secondCut = mid + (to - mid) / 2; firstCut = upperBound(items, from, mid, secondCut, comp); } int first2 = firstCut; int middle2 = mid; int last2 = secondCut; if (middle2 != first2 && middle2 != last2) { int first1 = first2; int last1 = middle2; while (first1 < --last1) {swap(items, first1++, last1);} first1 = middle2; last1 = last2; while (first1 < --last1) {swap(items, first1++, last1);} first1 = first2; last1 = last2; while (first1 < --last1) {swap(items, first1++, last1);} } mid = firstCut + secondCut - mid; inPlaceMerge(items, from, firstCut, mid, comp); inPlaceMerge(items, mid, secondCut, to, comp); } /** * Performs a binary search on an already-sorted range: finds the first position where an * element can be inserted without violating the ordering. Sorting is by a user-supplied * comparison function. * * @param items the int array to be sorted * @param from the index of the first element (inclusive) to be included in the binary search. * @param to the index of the last element (exclusive) to be included in the binary search. * @param pos the position of the element to be searched for. * @param comp the comparison function. * @return the largest index i such that, for every j in the range {@code [first..i)}, * {@code comp.compare(get(j), get(pos))} is {@code true}. */ private static int lowerBound (int[] items, int from, final int to, final int pos, final IntComparator comp) { int len = to - from; while (len > 0) { int half = len / 2; int middle = from + half; if (comp.compare(items[middle], items[pos]) < 0) { from = middle + 1; len -= half + 1; } else { len = half; } } return from; } /** * Performs a binary search on an already sorted range: finds the last position where an element * can be inserted without violating the ordering. Sorting is by a user-supplied comparison * function. * * @param items the int array to be sorted * @param from the index of the first element (inclusive) to be included in the binary search. * @param to the index of the last element (exclusive) to be included in the binary search. * @param pos the position of the element to be searched for. * @param comp the comparison function. * @return The largest index i such that, for every j in the range {@code [first..i)}, * {@code comp.compare(get(pos), get(j))} is {@code false}. */ private static int upperBound (int[] items, int from, final int to, final int pos, final IntComparator comp) { int len = to - from; while (len > 0) { int half = len / 2; int middle = from + half; if (comp.compare(items[pos], items[middle]) < 0) { len = half; } else { from = middle + 1; len -= half + 1; } } return from; } /** * Sorts all of {@code items} by simply calling {@link #sort(int[], int, int, IntComparator)}, * setting {@code from} and {@code to} so the whole array is sorted. * * @param items the int array to be sorted * @param c a IntComparator to alter the sort order; if null, the natural order will be used */ public static void sort (int[] items, final IntComparator c) { sort(items, 0, items.length, c); } /** * Sorts the specified range of elements according to the order induced by the specified * comparator using mergesort. * *

This sort is guaranteed to be stable: equal elements will not be reordered as a result * of the sort. The sorting algorithm is an in-place mergesort that is significantly slower than a * standard mergesort, as its running time is O(n (log n)2), * but it does not allocate additional memory; as a result, it can be * used as a generic sorting algorithm. * *

If and only if {@code c} is null, this will delegate to {@link Arrays#sort(int[], int, int)}, which * does not have the same guarantees regarding allocation. * * @param items the int array to be sorted * @param from the index of the first element (inclusive) to be sorted. * @param to the index of the last element (exclusive) to be sorted. * @param c a IntComparator to alter the sort order; if null, the natural order will be used */ public static void sort (int[] items, final int from, final int to, final IntComparator c) { if (to <= 0) { return; } if (from < 0 || from >= items.length || to > items.length) { throw new UnsupportedOperationException("The given from/to range in IntComparators.sort() is invalid."); } if (c == null) { Arrays.sort(items, from, to); return; } /* * We retain the same method signature as quickSort. Given only a comparator and this list * do not know how to copy and move elements from/to temporary arrays. Hence, in contrast to * the JDK mergesorts this is an "in-place" mergesort, i.e. does not allocate any temporary * arrays. A non-inplace mergesort would perhaps be faster in most cases, but would require * non-intuitive delegate objects... */ final int length = to - from; // Insertion sort on smallest arrays, less than 16 items if (length < 16) { for (int i = from; i < to; i++) { for (int j = i; j > from && c.compare(items[j - 1], items[j]) > 0; j--) { swap(items, j, j - 1); } } return; } // Recursively sort halves int mid = from + to >>> 1; sort(items, from, mid, c); sort(items, mid, to, c); // If list is already sorted, nothing left to do. This is an // optimization that results in faster sorts for nearly ordered lists. if (c.compare(items[mid - 1], items[mid]) <= 0) {return;} // Merge sorted halves inPlaceMerge(items, from, mid, to, c); } //// End of code primarily from FastUtil. /** * Analyzes {@code pixmap} for color count and frequency, building a palette with at most 256 colors if there are * too many colors to store in a PNG-8 palette. If there are 256 or less colors, this uses the exact colors * (although with at most one transparent color, and no alpha for other colors); this will always reserve a palette * entry for transparent (even if the image has no transparency) because it uses palette index 0 in its analysis * step. Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will dither colors that * aren't exact, and dithering works better when the palette can choose colors that are sufficiently different, this * takes a threshold value to determine whether it should permit a less-common color into the palette, and if the * second color is different enough (as measured by {@link #differenceAnalyzing(int, int)} ) by a value of at least * {@code threshold}, it is allowed in the palette, otherwise it is kept out for being too similar to existing * colors. The threshold is usually between 50 and 500, and 100 is a good default. Because this always uses the * maximum color limit, threshold should be lower than cases where the color limit is small. If the threshold is too * high, then some colors that would be useful to smooth out subtle color changes won't get considered, and colors * may change more abruptly. This doesn't return a value but instead stores the palette info in this object; a * PaletteReducer can be assigned to the {@link PNG8#palette} or {@link AnimatedGif#palette} fields or can be used * directly to {@link #reduce(Pixmap)} a Pixmap. * * @param pixmap a Pixmap to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)} or by PNG8 * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)} ; usually between 50 and 500, 100 is a good default */ public void analyze(Pixmap pixmap, double threshold) { analyze(pixmap, threshold, 256); } /** * Analyzes {@code pixmap} for color count and frequency, building a palette with at most {@code limit} colors. * If there are {@code limit} or fewer colors, this uses the exact colors (although with at most one transparent * color, and no alpha for other colors); this will always reserve a palette entry for transparent (even if the * image has no transparency) because it uses palette index 0 in its analysis step. Because calling * {@link #reduce(Pixmap)} (or any of PNG8's write methods) will dither colors that aren't exact, and dithering * works better when the palette can choose colors that are sufficiently different, this takes a threshold value to * determine whether it should permit a less-common color into the palette, and if the second color is different * enough (as measured by {@link #differenceAnalyzing(int, int)} ) by a value of at least {@code threshold}, it is allowed in * the palette, otherwise it is kept out for being too similar to existing colors. The threshold is usually between * 50 and 500, and 100 is a good default. If the threshold is too high, then some colors that would be useful to * smooth out subtle color changes won't get considered, and colors may change more abruptly. This doesn't return a * value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to {@link #reduce(Pixmap)} a * Pixmap. * * @param pixmap a Pixmap to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)} or by PNG8 * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ public void analyze(Pixmap pixmap, double threshold, int limit) { Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; limit = Math.min(Math.max(limit, 2), 256); threshold /= Math.min(0.45, Math.pow(limit + 16, 1.45) * 0.0002); final int width = pixmap.getWidth(), height = pixmap.getHeight(); IntIntMap counts = new IntIntMap(limit); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { color = pixmap.getPixel(x, y) & 0xF8F8F880; if ((color & 0x80) != 0) { color |= (color >>> 5 & 0x07070700) | 0xFF; counts.getAndIncrement(color, 0, 1); } } } int cs = counts.size; Array es = new Array<>(cs); for(IntIntMap.Entry e : counts) { IntIntMap.Entry e2 = new IntIntMap.Entry(); e2.key = e.key; e2.value = e.value; es.add(e2); } es.sort(entryComparator); if (cs < limit) { int i = 1; for(IntIntMap.Entry e : es) { color = e.key; paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } else // reduce color count { int i = 1, c = 0; PER_BEST: while (i < limit && c < cs) { color = es.get(c++).key; for (int j = 1; j < i; j++) { if (differenceAnalyzing(color, paletteArray[j]) < threshold) continue PER_BEST; } paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } int c2; int rr, gg, bb; double dist; for (int r = 0; r < 32; r++) { rr = (r << 3 | r >>> 2); for (int g = 0; g < 32; g++) { gg = (g << 3 | g >>> 2); for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (paletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = Double.MAX_VALUE; for (int i = 1; i < colorCount; i++) { if (dist > (dist = Math.min(dist, differenceAnalyzing(paletteArray[i], rr, gg, bb)))) paletteMapping[c2] = (byte) i; } } } } } } /** * Analyzes {@code pixmap} for color count and frequency, building a palette with at most {@code limit} colors. * If there are {@code limit} or fewer colors, this uses the exact colors (although with at most one transparent * color, and no alpha for other colors); this will always reserve a palette entry for transparent (even if the * image has no transparency) because it uses palette index 0 in its analysis step. Because calling * {@link #reduce(Pixmap)} (or any of PNG8's write methods) will dither colors that aren't exact, and dithering * works better when the palette can choose colors that are sufficiently different, this takes a threshold value to * determine whether it should permit a less-common color into the palette, and if the second color is different * enough (as measured by {@link #differenceHW(int, int)} ) by a value of at least {@code threshold}, it is allowed in * the palette, otherwise it is kept out for being too similar to existing colors. The threshold is usually between * 50 and 500, and 100 is a good default. If the threshold is too high, then some colors that would be useful to * smooth out subtle color changes won't get considered, and colors may change more abruptly. This doesn't return a * value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to {@link #reduce(Pixmap)} a * Pixmap. *
* The algorithm here isn't incredibly fast, but is often better at preserving colors that are used often enough to * be important to an image, but not often enough to appear in a small palette produced by {@link #analyze(Pixmap)}. * It involves sorting about 10% of the pixels in the image by hue, dividing up those pixels into evenly-sized * ranges, then sorting those ranges individually by lightness and dividing those into sub-ranges. The sub-ranges * have their chroma channels averaged (these already have similar hue, so this mostly affects saturation), and * their lightness averaged but pushed towards more extreme values using * {@link OtherMath#barronSpline(float, float, float)}. This last step works well with dithering. * * @param pixmap a Pixmap to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by PNG8, or by AnimatedGif * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ public void analyzeHueWise(Pixmap pixmap, double threshold, int limit) { Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; limit = Math.min(Math.max(limit, 3), 256); threshold /= Math.pow(limit, 1.35) * 0.000215; final int width = pixmap.getWidth(), height = pixmap.getHeight(); IntIntMap counts = new IntIntMap(limit); IntArray enc = new IntArray(width * height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { color = pixmap.getPixel(x, y) & 0xF8F8F880; if ((color & 0x80) != 0) { color |= (color >>> 5 & 0x07070700) | 0xFF; counts.getAndIncrement(color, 0, 1); if(((x & y) * 5 & 31) < 3) enc.add(shrink(color)); } } } int cs = counts.size; if (cs < limit) { Array es = new Array<>(cs); for(IntIntMap.Entry e : counts) { IntIntMap.Entry e2 = new IntIntMap.Entry(); e2.key = e.key; e2.value = e.value; es.add(e2); } es.sort(entryComparator); int i = 1; for(IntIntMap.Entry e : es) { color = e.key; paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } else // generate colors { final int[] ei = enc.items; sort(ei, 0, enc.size, hueComparator); paletteArray[1] = -1; // white paletteArray[2] = 255; // black int i = 3, encs = enc.size, segments = Math.min(encs, limit - 3) + 1 >> 1, e = 0; double lightPieces = Math.ceil(Math.log(limit)); PER_BEST: for (int s = 0; i < limit; s++) { if(e > (e %= encs)){ segments++; lightPieces++; threshold *= 0.9; } s %= segments; int segStart = e, segEnd = Math.min(segStart + (int)Math.ceil(encs / (double)segments), encs), segLen = segEnd - segStart; sort(ei, segStart, segLen, lightnessComparator); for (int li = 0; li < lightPieces && li < segLen && i < limit; li++) { int start = e, end = Math.min(encs, start + (int)Math.ceil(segLen / lightPieces)), len = end - start; float totalL = 0.0f, totalA = 0.0f, totalB = 0.0f; for (; e < end; e++) { int index = ei[e]; totalL += OKLAB[0][index]; totalA += OKLAB[1][index]; totalB += OKLAB[2][index]; } totalA /= len; totalB /= len; color = oklabToRGB( OtherMath.barronSpline(totalL / len, 3f, 0.5f), totalA,//(OtherMath.cbrt(totalA) + 31f * totalA) * 0x1p-5f, totalB,//(OtherMath.cbrt(totalB) + 31f * totalB) * 0x1p-5f, 1f); // (OtherMath.barronSpline(totalA / (len<<1)+0.5f, 2f, 0.5f)-0.5f)*2f, // (OtherMath.barronSpline(totalB / (len<<1)+0.5f, 2f, 0.5f)-0.5f)*2f, for (int j = 3; j < i; j++) { if (differenceHW(color, paletteArray[j]) < threshold) continue PER_BEST; } paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; i++; } } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } int c2; int rr, gg, bb; double dist; for (int r = 0; r < 32; r++) { rr = (r << 3 | r >>> 2); for (int g = 0; g < 32; g++) { gg = (g << 3 | g >>> 2); for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (paletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = Double.MAX_VALUE; for (int i = 1; i < colorCount; i++) { if (dist > (dist = Math.min(dist, differenceHW(paletteArray[i], rr, gg, bb)))) paletteMapping[c2] = (byte) i; } } } } } } /** * Analyzes {@code pixmap} for color count and frequency, building a palette with at most {@code limit} colors. * If there are {@code limit} or less colors, this uses the exact colors (although with at most one transparent * color, and no alpha for other colors); if there are more than {@code limit} colors or any colors have 50% or less * alpha, it will reserve a palette entry for transparent (even if the image has no transparency). Because calling * {@link #reduce(Pixmap)} (or any of PNG8's write methods) will dither colors that aren't exact, and dithering * works better when the palette can choose colors that are sufficiently different, this takes a threshold value to * determine whether it should permit a less-common color into the palette, and if the second color is different * enough (as measured by {@link #differenceAnalyzing(int, int)} ) by a value of at least {@code threshold}, it is allowed in * the palette, otherwise it is kept out for being too similar to existing colors. The threshold is usually between * 50 and 500, and 100 is a good default. If the threshold is too high, then some colors that would be useful to * smooth out subtle color changes won't get considered, and colors may change more abruptly. This doesn't return a * value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to {@link #reduce(Pixmap)} a * Pixmap. *
* This does a faster and less accurate analysis, and is more suitable to do on each frame of a large animation when * time is better spent making more images than fewer images at higher quality. It should be about 5 times faster * than {@link #analyze(Pixmap, double, int)} with the same parameters. * * @param pixmap a Pixmap to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)} or by PNG8 * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ public void analyzeFast(Pixmap pixmap, double threshold, int limit) { Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; limit = Math.min(Math.max(limit, 2), 256); threshold /= Math.min(0.45, Math.pow(limit + 16, 1.45) * 0.0002); final int width = pixmap.getWidth(), height = pixmap.getHeight(); IntIntMap counts = new IntIntMap(limit); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { color = pixmap.getPixel(x, y) & 0xF8F8F880; if ((color & 0x80) != 0) { color |= (color >>> 5 & 0x07070700) | 0xFF; counts.getAndIncrement(color, 0, 1); } } } int cs = counts.size; Array es = new Array<>(cs); for(IntIntMap.Entry e : counts) { IntIntMap.Entry e2 = new IntIntMap.Entry(); e2.key = e.key; e2.value = e.value; es.add(e2); } es.sort(entryComparator); if (cs < limit) { int i = 1; for(IntIntMap.Entry e : es) { color = e.key; paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; ++i; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } else // reduce color count { int i = 1, c = 0; PER_BEST: while (i < limit && c < cs) { color = es.get(c++).key; for (int j = 1; j < i; j++) { if (differenceAnalyzing(color, paletteArray[j]) < threshold) continue PER_BEST; } paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } if(colorCount <= 1) return; int c2; byte bt; int numUnassigned = 1, iterations = 0; byte[] buffer = Arrays.copyOf(paletteMapping, 0x8000); while (numUnassigned != 0) { numUnassigned = 0; for (int r = 0; r < 32; r++) { for (int g = 0; g < 32; g++) { for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (buffer[c2] == 0) { if(iterations++ != 2){ if (b < 31 && (bt = paletteMapping[c2 + 1]) != 0) buffer[c2] = bt; else if (g < 31 && (bt = paletteMapping[c2 + 32]) != 0) buffer[c2] = bt; else if (r < 31 && (bt = paletteMapping[c2 + 1024]) != 0) buffer[c2] = bt; else if (b > 0 && (bt = paletteMapping[c2 - 1]) != 0) buffer[c2] = bt; else if (g > 0 && (bt = paletteMapping[c2 - 32]) != 0) buffer[c2] = bt; else if (r > 0 && (bt = paletteMapping[c2 - 1024]) != 0) buffer[c2] = bt; else numUnassigned++; } else { iterations = 0; if (b < 31 && (bt = paletteMapping[c2 + 1]) != 0) buffer[c2] = bt; else if (g < 31 && (bt = paletteMapping[c2 + 32]) != 0) buffer[c2] = bt; else if (r < 31 && (bt = paletteMapping[c2 + 1024]) != 0) buffer[c2] = bt; else if (b > 0 && (bt = paletteMapping[c2 - 1]) != 0) buffer[c2] = bt; else if (g > 0 && (bt = paletteMapping[c2 - 32]) != 0) buffer[c2] = bt; else if (r > 0 && (bt = paletteMapping[c2 - 1024]) != 0) buffer[c2] = bt; else if (b < 31 && g < 31 && (bt = paletteMapping[c2 + 1 + 32]) != 0) buffer[c2] = bt; else if (b < 31 && r < 31 && (bt = paletteMapping[c2 + 1 + 1024]) != 0) buffer[c2] = bt; else if (g < 31 && r < 31 && (bt = paletteMapping[c2 + 32 + 1024]) != 0) buffer[c2] = bt; else if (b > 0 && g > 0 && (bt = paletteMapping[c2 - 1 - 32]) != 0) buffer[c2] = bt; else if (b > 0 && r > 0 && (bt = paletteMapping[c2 - 1 - 1024]) != 0) buffer[c2] = bt; else if (g > 0 && r > 0 && (bt = paletteMapping[c2 - 32 - 1024]) != 0) buffer[c2] = bt; else if (b < 31 && g > 0 && (bt = paletteMapping[c2 + 1 - 32]) != 0) buffer[c2] = bt; else if (b < 31 && r > 0 && (bt = paletteMapping[c2 + 1 - 1024]) != 0) buffer[c2] = bt; else if (g < 31 && r > 0 && (bt = paletteMapping[c2 + 32 - 1024]) != 0) buffer[c2] = bt; else if (b > 0 && g < 31 && (bt = paletteMapping[c2 - 1 + 32]) != 0) buffer[c2] = bt; else if (b > 0 && r < 31 && (bt = paletteMapping[c2 - 1 + 1024]) != 0) buffer[c2] = bt; else if (g > 0 && r < 31 && (bt = paletteMapping[c2 - 32 + 1024]) != 0) buffer[c2] = bt; else numUnassigned++; } } } } } System.arraycopy(buffer, 0, paletteMapping, 0, 0x8000); } } public void analyzeMC(Pixmap pixmap, int limit) { Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; final int width = pixmap.getWidth(), height = pixmap.getHeight(); IntArray bin = new IntArray(width * height); IntIntMap counts = new IntIntMap(limit); int hasTransparent = 0; int rangeR, rangeG, rangeB; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { color = pixmap.getPixel(x, y) & 0xF8F8F880; if ((color & 0x80) != 0) { bin.add(color |= (color >>> 5 & 0x07070700) | 0xFF); counts.getAndIncrement(color, 0, 1); } else { hasTransparent = 1; } } } limit = Math.max(2 - hasTransparent, Math.min(limit - hasTransparent, 256)); if(counts.size > limit) { int numCuts = 32 - Integer.numberOfLeadingZeros(limit - 1); int offset, end = bin.size; int[] in = bin.items, out = new int[end], bufR = new int[32], bufG = new int[32], bufB = new int[32]; for (int stage = 0; stage < numCuts; stage++) { int size = bin.size >>> stage; offset = 0; end = 0; for (int part = 1 << stage; part > 0; part--) { if (part == 1) end = bin.size; else end += size; Arrays.fill(bufR, 0); Arrays.fill(bufG, 0); Arrays.fill(bufB, 0); for (int i = offset, ii; i < end; i++) { ii = in[i]; bufR[ii >>> 27]++; bufG[ii >>> 19 & 31]++; bufB[ii >>> 11 & 31]++; } for (rangeR = 32; rangeR > 0 && bufR[rangeR - 1] == 0; rangeR--) ; for (int r = 0; r < rangeR && bufR[r] == 0; r++, rangeR--) ; for (rangeG = 32; rangeG > 0 && bufG[rangeG - 1] == 0; rangeG--) ; for (int r = 0; r < rangeG && bufG[r] == 0; r++, rangeG--) ; for (rangeB = 32; rangeB > 0 && bufB[rangeB - 1] == 0; rangeB--) ; for (int r = 0; r < rangeB && bufB[r] == 0; r++, rangeB--) ; if (rangeG >= rangeR && rangeG >= rangeB) { for (int i = 1; i < 32; i++) bufG[i] += bufG[i - 1]; for (int i = end - 1; i >= offset; i--) out[offset + --bufG[in[i] >>> 19 & 31]] = in[i]; } else if (rangeR >= rangeG && rangeR >= rangeB) { for (int i = 1; i < 32; i++) bufR[i] += bufR[i - 1]; for (int i = end - 1; i >= offset; i--) out[offset + --bufR[in[i] >>> 27]] = in[i]; } else { for (int i = 1; i < 32; i++) bufB[i] += bufB[i - 1]; for (int i = end - 1; i >= offset; i--) out[offset + --bufB[in[i] >>> 11 & 31]] = in[i]; } offset += size; } } int jump = out.length >>> numCuts, mid = 0, assigned = 0; double fr = 270.0 / (jump * 31.0); for (int n = (1 << numCuts) - 1; assigned < n; assigned++, mid += jump) { double r = 0, g = 0, b = 0; for (int i = mid + jump - 1; i >= mid; i--) { color = out[i]; r += color >>> 27; g += color >>> 19 & 31; b += color >>> 11 & 31; } paletteArray[assigned] = Math.min(Math.max((int)((r - 7.0) * fr), 0), 255) << 24 | Math.min(Math.max((int)((g - 7.0) * fr), 0), 255) << 16 | Math.min(Math.max((int)((b - 7.0) * fr), 0), 255) << 8 | 0xFF; } { int j2 = out.length - (mid - jump); double r = 0, g = 0, b = 0, fr2 = 270.0 / (j2 * 31.0); for (int i = out.length - 1; i >= mid; i--) { color = out[i]; r += color >>> 27; g += color >>> 19 & 31; b += color >>> 11 & 31; } paletteArray[assigned++] = Math.min(Math.max((int)((r - 7.0) * fr2), 0), 255) << 24 | Math.min(Math.max((int)((g - 7.0) * fr2), 0), 255) << 16 | Math.min(Math.max((int)((b - 7.0) * fr2), 0), 255) << 8 | 0xFF; } // int jump = out.length >>> numCuts, mid = jump >>> 1, assigned = 0; // for (int n = 1 << numCuts; assigned < n; assigned++, mid += jump) { // paletteArray[assigned] = out[mid]; // } COLORS: for (int i = limit; i < assigned; i++) { int currentCount = counts.get(paletteArray[i], 0); for (int j = 0; j < limit; j++) { if(counts.get(paletteArray[j], 0) < currentCount) { int temp = paletteArray[j]; paletteArray[j] = paletteArray[i]; paletteArray[i] = temp; continue COLORS; } } } if(hasTransparent == 1) { int min = Integer.MAX_VALUE, worst = 0; for (int i = 0; i < limit; i++) { int currentCount = counts.get(paletteArray[i], 0); if(currentCount < min){ min = currentCount; worst = i; } } if (worst != 0) { paletteArray[worst] = paletteArray[0]; } paletteArray[0] = 0; } // COLORS: // for (; mid < out.length; mid += jump) { // int currentCount = counts.get(out[mid], 0); // for (int i = limit - 1; i > hasTransparent; i--) { // if(counts.get(paletteArray[i], 0) < currentCount) // { // paletteArray[i] = out[mid]; // continue COLORS; // } // } // } for (int i = hasTransparent; i < limit; i++) { color = paletteArray[i]; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; } colorCount = limit; populationBias = (float) Math.exp(-1.125/colorCount); } else { IntIntMap.Keys it = counts.keys(); Arrays.fill(paletteArray, 0); for (int i = hasTransparent; i < limit && it.hasNext; i++) { paletteArray[i] = color = it.next(); paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; } colorCount = counts.size + hasTransparent; populationBias = (float) Math.exp(-1.125/colorCount); } if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } int c2; int rr, gg, bb; double dist; for (int r = 0; r < 32; r++) { rr = (r << 3 | r >>> 2); for (int g = 0; g < 32; g++) { gg = (g << 3 | g >>> 2); for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (paletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = Double.POSITIVE_INFINITY; for (int i = 1; i < colorCount; i++) { if (dist > (dist = Math.min(dist, differenceAnalyzing(paletteArray[i], rr, gg, bb)))) paletteMapping[c2] = (byte) i; } } } } } } public int blend(int rgba1, int rgba2, float preference) { int a1 = rgba1 & 255, a2 = rgba2 & 255; if((a1 & 0x80) == 0) return rgba2; else if((a2 & 0x80) == 0) return rgba1; rgba1 = shrink(rgba1); rgba2 = shrink(rgba2); float L = OKLAB[0][rgba1] + (OKLAB[0][rgba2] - OKLAB[0][rgba1]) * preference; float A = OKLAB[1][rgba1] + (OKLAB[1][rgba2] - OKLAB[1][rgba1]) * preference; float B = OKLAB[2][rgba1] + (OKLAB[2][rgba2] - OKLAB[2][rgba1]) * preference; return oklabToRGB(L, A, B, (a1 + (a2 - a1) * preference) * (1f/255f)); } /** * Analyzes all the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most 256 colors. If there are 256 or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * 256 colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by * {@link #differenceAnalyzing(int, int, int, int)}) by a * value of at least 100, it is allowed in the palette, otherwise it is kept out for being too similar to existing * colors. This doesn't return a value but instead stores the palette info in this object; a PaletteReducer can be * assigned to the {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap Array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 */ public void analyze(Array pixmaps){ analyze(pixmaps.toArray(Pixmap.class), pixmaps.size, 100, 256); } /** * Analyzes all the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most 256 colors. If there are 256 or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * 256 colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by {@link #differenceAnalyzing(int, int)}) by a * value of at least {@code threshold}, it is allowed in the palette, otherwise it is kept out for being too similar * to existing colors. The threshold is usually between 50 and 500, and 100 is a good default. This doesn't return * a value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap Array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default */ public void analyze(Array pixmaps, double threshold){ analyze(pixmaps.toArray(Pixmap.class), pixmaps.size, threshold, 256); } /** * Analyzes all the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most {@code limit} colors. If there are {@code limit} or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * {@code limit} colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by {@link #differenceAnalyzing(int, int)}) by a * value of at least {@code threshold}, it is allowed in the palette, otherwise it is kept out for being too similar * to existing colors. The threshold is usually between 50 and 500, and 100 is a good default. This doesn't return * a value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap Array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ public void analyze(Array pixmaps, double threshold, int limit){ analyze(pixmaps.toArray(Pixmap.class), pixmaps.size, threshold, limit); } /** * Analyzes all the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most {@code limit} colors. If there are {@code limit} or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * {@code limit} colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by {@link #differenceAnalyzing(int, int)}) by a * value of at least {@code threshold}, it is allowed in the palette, otherwise it is kept out for being too similar * to existing colors. The threshold is usually between 50 and 500, and 100 is a good default. This doesn't return * a value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 * @param pixmapCount the maximum number of Pixmap entries in pixmaps to use * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ public void analyze(Pixmap[] pixmaps, int pixmapCount, double threshold, int limit) { Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; limit = Math.min(Math.max(limit, 2), 256); threshold /= Math.min(0.45, Math.pow(limit + 16, 1.45) * 0.0002); IntIntMap counts = new IntIntMap(limit); int[] reds = new int[limit], greens = new int[limit], blues = new int[limit]; for (int i = 0; i < pixmapCount && i < pixmaps.length; i++) { Pixmap pixmap = pixmaps[i]; final int width = pixmap.getWidth(), height = pixmap.getHeight(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { color = pixmap.getPixel(x, y) & 0xF8F8F880; if ((color & 0x80) != 0) { color |= (color >>> 5 & 0x07070700) | 0xFF; counts.getAndIncrement(color, 0, 1); } } } } final int cs = counts.size; Array es = new Array<>(cs); for(IntIntMap.Entry e : counts) { IntIntMap.Entry e2 = new IntIntMap.Entry(); e2.key = e.key; e2.value = e.value; es.add(e2); } es.sort(entryComparator); if (cs < limit) { int i = 1; for(IntIntMap.Entry e : es) { color = e.key; paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; reds[i] = color >>> 24; greens[i] = color >>> 16 & 255; blues[i] = color >>> 8 & 255; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } else // reduce color count { int i = 1, c = 0; PER_BEST: for (; i < limit && c < cs;) { color = es.get(c++).key; for (int j = 1; j < i; j++) { double diff = differenceAnalyzing(color, paletteArray[j]); if (diff < threshold) continue PER_BEST; } paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; reds[i] = color >>> 24; greens[i] = color >>> 16 & 255; blues[i] = color >>> 8 & 255; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } int c2; int rr, gg, bb; double dist; for (int r = 0; r < 32; r++) { rr = (r << 3 | r >>> 2); for (int g = 0; g < 32; g++) { gg = (g << 3 | g >>> 2); for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (paletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = Double.MAX_VALUE; for (int i = 1; i < colorCount; i++) { if (dist > (dist = Math.min(dist, differenceAnalyzing(reds[i], greens[i], blues[i], rr, gg, bb)))) paletteMapping[c2] = (byte) i; } } } } } } /** * Analyzes all the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most 256 colors. If there are 256 or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * 256 colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by * {@link #differenceHW(int, int, int, int)}) by a * value of at least 100, it is allowed in the palette, otherwise it is kept out for being too similar to existing * colors. This doesn't return a value but instead stores the palette info in this object; a PaletteReducer can be * assigned to the {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap Array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 */ public void analyzeHueWise(Array pixmaps){ analyzeHueWise(pixmaps.toArray(Pixmap.class), pixmaps.size, 100, 256); } /** * Analyzes all of the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most 256 colors. If there are 256 or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * 256 colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by {@link #differenceHW(int, int)}) by a * value of at least {@code threshold}, it is allowed in the palette, otherwise it is kept out for being too similar * to existing colors. The threshold is usually between 50 and 500, and 100 is a good default. This doesn't return * a value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap Array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 * @param threshold a minimum color difference as produced by {@link #differenceHW(int, int)}; usually between 50 and 500, 100 is a good default */ public void analyzeHueWise(Array pixmaps, double threshold){ analyzeHueWise(pixmaps.toArray(Pixmap.class), pixmaps.size, threshold, 256); } /** * Analyzes all of the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most {@code limit} colors. If there are {@code limit} or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * {@code limit} colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by {@link #differenceHW(int, int)}) by a * value of at least {@code threshold}, it is allowed in the palette, otherwise it is kept out for being too similar * to existing colors. The threshold is usually between 50 and 500, and 100 is a good default. This doesn't return * a value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap Array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 * @param threshold a minimum color difference as produced by {@link #differenceHW(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ public void analyzeHueWise(Array pixmaps, double threshold, int limit){ analyzeHueWise(pixmaps.toArray(Pixmap.class), pixmaps.size, threshold, limit); } /** * Analyzes all of the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most {@code limit} colors. If there are {@code limit} or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * {@code limit} colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by {@link #differenceHW(int, int)}) by a * value of at least {@code threshold}, it is allowed in the palette, otherwise it is kept out for being too similar * to existing colors. The threshold is usually between 50 and 500, and 100 is a good default. This doesn't return * a value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 * @param pixmapCount the maximum number of Pixmap entries in pixmaps to use * @param threshold a minimum color difference as produced by {@link #differenceHW(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ public void analyzeHueWise(Pixmap[] pixmaps, int pixmapCount, double threshold, int limit) { Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; limit = Math.min(Math.max(limit, 3), 256); threshold /= Math.pow(limit, 1.35) * 0.000215; final int w0 = pixmaps[0].getWidth(), h0 = pixmaps[0].getHeight(); IntIntMap counts = new IntIntMap(limit); IntArray enc = new IntArray(w0 * h0 * pixmapCount / 10); int[] reds = new int[limit], greens = new int[limit], blues = new int[limit]; for (int i = 0; i < pixmapCount && i < pixmaps.length; i++) { Pixmap pixmap = pixmaps[i]; final int width = pixmap.getWidth(), height = pixmap.getHeight(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { color = pixmap.getPixel(x, y) & 0xF8F8F880; if ((color & 0x80) != 0) { color |= (color >>> 5 & 0x07070700) | 0xFF; counts.getAndIncrement(color, 0, 1); if(((x & y) * 5 - i & 31) < 3) enc.add(shrink(color)); } } } } final int cs = counts.size; if (cs < limit) { Array es = new Array<>(cs); for(IntIntMap.Entry e : counts) { IntIntMap.Entry e2 = new IntIntMap.Entry(); e2.key = e.key; e2.value = e.value; es.add(e2); } es.sort(entryComparator); int i = 1; for(IntIntMap.Entry e : es) { color = e.key; paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; reds[i] = color >>> 24; greens[i] = color >>> 16 & 255; blues[i] = color >>> 8 & 255; i++; } colorCount = i; } else // generate colors { final int[] ei = enc.items; sort(ei, 0, enc.size, hueComparator); paletteArray[1] = -1; // white reds[1] = 255; greens[1] = 255; blues[1] = 255; paletteArray[2] = 255; // black reds[2] = 0; greens[2] = 0; blues[2] = 0; int i = 3, encs = enc.size, segments = Math.min(encs, limit - 3) + 1 >> 1, e = 0; double lightPieces = Math.ceil(Math.log(limit)); PER_BEST: for (int s = 0; i < limit; s++) { if(e > (e %= encs)){ segments++; lightPieces++; threshold *= 0.9; } s %= segments; int segStart = e, segEnd = Math.min(segStart + (int)Math.ceil(encs / (double)segments), encs), segLen = segEnd - segStart; sort(ei, segStart, segLen, lightnessComparator); for (int li = 0; li < lightPieces && li < segLen && i < limit; li++) { int start = e, end = Math.min(encs, start + (int)Math.ceil(segLen / lightPieces)), len = end - start; float totalL = 0.0f, totalA = 0.0f, totalB = 0.0f; for (; e < end; e++) { int index = ei[e]; totalL += OKLAB[0][index]; totalA += OKLAB[1][index]; totalB += OKLAB[2][index]; } totalA /= len; totalB /= len; color = oklabToRGB( OtherMath.barronSpline(totalL / len, 3f, 0.5f), totalA,//(OtherMath.cbrt(totalA) + 31f * totalA) * 0x1p-5f, totalB,//(OtherMath.cbrt(totalB) + 31f * totalB) * 0x1p-5f, 1f); // (OtherMath.barronSpline(totalA / (len<<1)+0.5f, 2f, 0.5f)-0.5f)*2f, // (OtherMath.barronSpline(totalB / (len<<1)+0.5f, 2f, 0.5f)-0.5f)*2f, for (int j = 3; j < i; j++) { if (differenceHW(color, paletteArray[j]) < threshold) continue PER_BEST; } paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; reds[i] = color >>> 24; greens[i] = color >>> 16 & 255; blues[i] = color >>> 8 & 255; i++; } } colorCount = i; } populationBias = (float) Math.exp(-1.125/colorCount); if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } int c2; int rr, gg, bb; double dist; for (int r = 0; r < 32; r++) { rr = (r << 3 | r >>> 2); for (int g = 0; g < 32; g++) { gg = (g << 3 | g >>> 2); for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (paletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = Double.MAX_VALUE; for (int i = 1; i < colorCount; i++) { if (dist > (dist = Math.min(dist, differenceHW(reds[i], greens[i], blues[i], rr, gg, bb)))) paletteMapping[c2] = (byte) i; } } } } } } /** * Gets the "strength" of the dither effect applied during {@link #reduce(Pixmap)} calls. The default is 1f, * and while both values higher than 1f and lower than 1f are valid, they should not be negative. * If ditherStrength is too high, all sorts of artifacts will appear; if it is too low, the effect of the dither to * smooth out changes in color will be very hard to notice. * @return the current dither strength; typically near 1.0f and always non-negative */ public float getDitherStrength() { return ditherStrength; } /** * Changes the "strength" of the dither effect applied during {@link #reduce(Pixmap)} calls. The default is 1f, * and while both values higher than 1f and lower than 1f are valid, they should not be negative. If you want dither * to be eliminated, don't set dither strength to 0; use {@link #reduceSolid(Pixmap)} instead of reduce(). * If ditherStrength is too high, all sorts of artifacts will appear; if it is too low, the effect of the dither to * smooth out changes in color will be very hard to notice. * @param ditherStrength dither strength as a non-negative float that should be close to 1f */ public void setDitherStrength(float ditherStrength) { this.ditherStrength = Math.max(0f, ditherStrength); } /** * Modifies the given Pixmap so it only uses colors present in this PaletteReducer, dithering when it can * using Neue dithering (this merely delegates to {@link #reduceNeue(Pixmap)}). * If you want to reduce the colors in a Pixmap based on what it currently contains, call * {@link #analyze(Pixmap)} with {@code pixmap} as its argument, then call this method with the same * Pixmap. You may instead want to use a known palette instead of one computed from a Pixmap; * {@link #exact(int[])} is the tool for that job. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduce (Pixmap pixmap) { return reduceNeue(pixmap); } /** * Uses the given {@link Dithered.DitherAlgorithm} to decide how to dither {@code pixmap}. * @param pixmap a pixmap that will be modified in-place * @param ditherAlgorithm a dithering algorithm enum value; if not recognized, defaults to {@link Dithered.DitherAlgorithm#NEUE} * @return {@code pixmap} after modifications */ public Pixmap reduce(Pixmap pixmap, Dithered.DitherAlgorithm ditherAlgorithm){ if(pixmap == null) return null; if(ditherAlgorithm == null) return reduceNeue(pixmap); switch (ditherAlgorithm) { case NONE: return reduceSolid(pixmap); case GRADIENT_NOISE: return reduceJimenez(pixmap); case PATTERN: return reduceKnoll(pixmap); case CHAOTIC_NOISE: return reduceChaoticNoise(pixmap); case DIFFUSION: return reduceFloydSteinberg(pixmap); case BLUE_NOISE: return reduceBlueNoise(pixmap); case SCATTER: return reduceScatter(pixmap); case ROBERTS: return reduceRoberts(pixmap); case WOVEN: return reduceWoven(pixmap); default: case NEUE: return reduceNeue(pixmap); } } /** * Modifies the given Pixmap so it only uses colors present in this PaletteReducer, without dithering. This produces * blocky solid sections of color in most images where the palette isn't exact, instead of checkerboard-like * dithering patterns. If you want to reduce the colors in a Pixmap based on what it currently contains, call * {@link #analyze(Pixmap)} with {@code pixmap} as its argument, then call this method with the same * Pixmap. You may instead want to use a known palette instead of one computed from a Pixmap; * {@link #exact(int[])} is the tool for that job. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceSolid (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color; for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { int rr = ((color >>> 24) ); int gg = ((color >>> 16) & 0xFF); int bb = ((color >>> 8) & 0xFF); pixmap.drawPixel(px, y, paletteArray[ paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]); } } } pixmap.setBlending(blending); return pixmap; } /** * Modifies the given Pixmap so that it only uses colors present in this PaletteReducer, dithering when it can with * Sierra Lite dithering instead of the Floyd-Steinberg dithering that {@link #reduce(Pixmap)} uses. * If you want to reduce the colors in a Pixmap based on what it currently contains, call * {@link #analyze(Pixmap)} with {@code pixmap} as its argument, then call this method with the same * Pixmap. You may instead want to use a known palette instead of one computed from a Pixmap; * {@link #exact(int[])} is the tool for that job. *

* This method is similar to Floyd-Steinberg, since both are error-diffusion dithers. Sometimes Sierra Lite can * avoid unpleasant artifacts in Floyd-Steinberg, so it's better in the worst-case, but it isn't usually as good in * its best-case. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceSierraLite (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; float rdiff, gdiff, bdiff; float er, eg, eb; float ditherStrength = this.ditherStrength * 20, halfDitherStrength = ditherStrength * 0.5f; for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { er = curErrorRed[px]; eg = curErrorGreen[px]; eb = curErrorBlue[px]; int rr = Math.min(Math.max((int)(((color >>> 24) ) + er + 0.5f), 0), 0xFF); int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + eg + 0.5f), 0), 0xFF); int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + eb + 0.5f), 0), 0xFF); used = paletteArray[paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]; pixmap.drawPixel(px, y, used); rdiff = (0x2.4p-8f * ((color>>>24)- (used>>>24)) ); gdiff = (0x2.4p-8f * ((color>>>16&255)-(used>>>16&255))); bdiff = (0x2.4p-8f * ((color>>>8&255)- (used>>>8&255)) ); rdiff *= 1.25f / (0.25f + Math.abs(rdiff)); gdiff *= 1.25f / (0.25f + Math.abs(gdiff)); bdiff *= 1.25f / (0.25f + Math.abs(bdiff)); if(px < lineLen - 1) { curErrorRed[px+1] += rdiff * ditherStrength; curErrorGreen[px+1] += gdiff * ditherStrength; curErrorBlue[px+1] += bdiff * ditherStrength; } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += rdiff * halfDitherStrength; nextErrorGreen[px-1] += gdiff * halfDitherStrength; nextErrorBlue[px-1] += bdiff * halfDitherStrength; } nextErrorRed[px] += rdiff * halfDitherStrength; nextErrorGreen[px] += gdiff * halfDitherStrength; nextErrorBlue[px] += bdiff * halfDitherStrength; } } } } pixmap.setBlending(blending); return pixmap; } /** * Modifies the given Pixmap so that it only uses colors present in this PaletteReducer, dithering when it can with * the commonly-used Floyd-Steinberg dithering. If you want to reduce the colors in a Pixmap based on what it * currently contains, call {@link #analyze(Pixmap)} with {@code pixmap} as its argument, then call this method with * the same Pixmap. You may instead want to use a known palette instead of one computed from a Pixmap; * {@link #exact(int[])} is the tool for that job. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceFloydSteinberg (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; float rdiff, gdiff, bdiff; float er, eg, eb; float w1 = ditherStrength * 4, w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f; for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { er = curErrorRed[px]; eg = curErrorGreen[px]; eb = curErrorBlue[px]; int rr = Math.min(Math.max((int)(((color >>> 24) ) + er + 0.5f), 0), 0xFF); int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + eg + 0.5f), 0), 0xFF); int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + eb + 0.5f), 0), 0xFF); used = paletteArray[paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]; pixmap.drawPixel(px, y, used); rdiff = (0x1.8p-8f * ((color>>>24)- (used>>>24)) ); gdiff = (0x1.8p-8f * ((color>>>16&255)-(used>>>16&255))); bdiff = (0x1.8p-8f * ((color>>>8&255)- (used>>>8&255)) ); rdiff *= 1.25f / (0.25f + Math.abs(rdiff)); gdiff *= 1.25f / (0.25f + Math.abs(gdiff)); bdiff *= 1.25f / (0.25f + Math.abs(bdiff)); if(px < lineLen - 1) { curErrorRed[px+1] += rdiff * w7; curErrorGreen[px+1] += gdiff * w7; curErrorBlue[px+1] += bdiff * w7; } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += rdiff * w3; nextErrorGreen[px-1] += gdiff * w3; nextErrorBlue[px-1] += bdiff * w3; } if(px < lineLen - 1) { nextErrorRed[px+1] += rdiff * w1; nextErrorGreen[px+1] += gdiff * w1; nextErrorBlue[px+1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } } pixmap.setBlending(blending); return pixmap; } /** * It's interleaved gradient noise, by Jorge Jimenez! It's very fast! It's an ordered dither! * It's pretty good with gradients, though it may introduce artifacts. It has noticeable diagonal * lines in some places, but these tend to have mixed directions that obscure larger patterns. * This is very similar to {@link #reduceRoberts(Pixmap)}, but has different artifacts, and this * dither tends to be stronger by default. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceJimenez(Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color; float adj; final float strength = 60f * ditherStrength / (populationBias * populationBias); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { adj = (px * 0.06711056f + y * 0.00583715f); adj -= (int) adj; adj *= 52.9829189f; adj -= (int) adj; adj -= 0.5f; adj *= strength; // adj *= adj * adj; // adj *= Math.abs(adj); // adj = Math.copySign((float) Math.sqrt(Math.abs(adj)), adj); adj += 0.5f; // for rounding int rr = Math.min(Math.max((int)(((color >>> 24) ) + adj), 0), 255); int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + adj), 0), 255); int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + adj), 0), 255); pixmap.drawPixel(px, y, paletteArray[paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]); } } } pixmap.setBlending(blending); return pixmap; } public Pixmap reduceIgneous(Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; float rdiff, gdiff, bdiff; float er, eg, eb; byte paletteIndex; float w1 = (6f * ditherStrength * populationBias * populationBias), w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f, strength = 60f * ditherStrength / (populationBias * populationBias), adj; for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { adj = (px * 0.06711056f + y * 0.00583715f); adj -= (int) adj; adj *= 52.9829189f; adj -= (int) adj; adj -= 0.5f; adj *= strength; er = adj + (curErrorRed[px]); eg = adj + (curErrorGreen[px]); eb = adj + (curErrorBlue[px]); int rr = MathUtils.clamp((int)(((color >>> 24) ) + er + 0.5f), 0, 0xFF); int gg = MathUtils.clamp((int)(((color >>> 16) & 0xFF) + eg + 0.5f), 0, 0xFF); int bb = MathUtils.clamp((int)(((color >>> 8) & 0xFF) + eb + 0.5f), 0, 0xFF); paletteIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))]; used = paletteArray[paletteIndex & 0xFF]; pixmap.drawPixel(px, y, used); rdiff = (0x3p-10f * ((color>>>24)- (used>>>24)) ); gdiff = (0x3p-10f * ((color>>>16&255)-(used>>>16&255))); bdiff = (0x3p-10f * ((color>>>8&255)- (used>>>8&255)) ); if(px < lineLen - 1) { curErrorRed[px+1] += rdiff * w7; curErrorGreen[px+1] += gdiff * w7; curErrorBlue[px+1] += bdiff * w7; } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += rdiff * w3; nextErrorGreen[px-1] += gdiff * w3; nextErrorBlue[px-1] += bdiff * w3; } if(px < lineLen - 1) { nextErrorRed[px+1] += rdiff * w1; nextErrorGreen[px+1] += gdiff * w1; nextErrorBlue[px+1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } } pixmap.setBlending(blending); return pixmap; } /** * An ordered dither that uses a sub-random sequence by Martin Roberts to disperse lightness adjustments across the * image. This is very similar to {@link #reduceJimenez(Pixmap)}, but is milder by default, and has subtly different * artifacts. This should look excellent for animations, especially with small palettes, but the lightness * adjustments may be noticeable even in very large palettes. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceRoberts (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color; // float str = (32f * ditherStrength / (populationBias * populationBias)); // float str = (float) (64 * ditherStrength / Math.log(colorCount * 0.3 + 1.5)); float str = (25f * ditherStrength / (populationBias * populationBias * populationBias * populationBias)); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { // used in 0.3.10 // // Gets R2-based noise and puts it in the -0.75 to 0.75 range // float adj = (px * 0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL >>> 41) * 0x1.8p-23f - 0.75f; // adj = adj * str + 0.5f; // int rr = Math.min(Math.max((int)(((color >>> 24) ) + adj), 0), 255); // int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + adj), 0), 255); // int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + adj), 0), 255); // other options // // sign-preserving square root, emphasizes extremes //// adj = Math.copySign((float) Math.sqrt(Math.abs(adj)), adj); // // sign-preserving square, emphasizes low-magnitude values //// adj *= Math.abs(adj); int rr = Math.min(Math.max((int)(((color >>> 24) ) + ((((px-1) * 0xC13FA9A902A6328FL + (y+1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-22f - 0x1.4p0f) * str + 0.5f), 0), 255); int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + ((((px+3) * 0xC13FA9A902A6328FL + (y-1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-22f - 0x1.4p0f) * str + 0.5f), 0), 255); int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + ((((px-4) * 0xC13FA9A902A6328FL + (y+2) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-22f - 0x1.4p0f) * str + 0.5f), 0), 255); pixmap.drawPixel(px, y, paletteArray[paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]); } } } pixmap.setBlending(blending); return pixmap; } public Pixmap reduceWoven(Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; float rdiff, gdiff, bdiff; float er, eg, eb; byte paletteIndex; float w1 = (float) (20f * Math.sqrt(ditherStrength) * populationBias * populationBias * populationBias * populationBias), w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f, strength = 48f * ditherStrength / (populationBias * populationBias * populationBias * populationBias), limit = 5f + 110f / (float)Math.sqrt(colorCount+1.5f); for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { er = Math.min(Math.max(((((px+1) * 0xC13FA9A902A6328FL + (y+1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-23f - 0x1.4p-1f) * strength, -limit), limit) + (curErrorRed[px]); eg = Math.min(Math.max(((((px+3) * 0xC13FA9A902A6328FL + (y-1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-23f - 0x1.4p-1f) * strength, -limit), limit) + (curErrorGreen[px]); eb = Math.min(Math.max(((((px-4) * 0xC13FA9A902A6328FL + (y+2) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-23f - 0x1.4p-1f) * strength, -limit), limit) + (curErrorBlue[px]); int rr = Math.min(Math.max((int)(((color >>> 24) ) + er + 0.5f), 0), 0xFF); int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + eg + 0.5f), 0), 0xFF); int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + eb + 0.5f), 0), 0xFF); paletteIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))]; used = paletteArray[paletteIndex & 0xFF]; pixmap.drawPixel(px, y, used); rdiff = (0x5p-10f * ((color>>>24)- (used>>>24)) ); gdiff = (0x5p-10f * ((color>>>16&255)-(used>>>16&255))); bdiff = (0x5p-10f * ((color>>>8&255)- (used>>>8&255)) ); if(px < lineLen - 1) { curErrorRed[px+1] += rdiff * w7; curErrorGreen[px+1] += gdiff * w7; curErrorBlue[px+1] += bdiff * w7; } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += rdiff * w3; nextErrorGreen[px-1] += gdiff * w3; nextErrorBlue[px-1] += bdiff * w3; } if(px < lineLen - 1) { nextErrorRed[px+1] += rdiff * w1; nextErrorGreen[px+1] += gdiff * w1; nextErrorBlue[px+1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } } pixmap.setBlending(blending); return pixmap; } /** * A blue-noise-based dither; does not diffuse error, and uses a tiling blue noise pattern (which can be accessed * with {@link #TRI_BLUE_NOISE}, but shouldn't usually be modified) as well as a 8x8 threshold matrix (the kind * used by {@link #reduceKnoll(Pixmap)}, but larger). This has a tendency to look closer to a color * reduction with no dither (as with {@link #reduceSolid(Pixmap)} than to one with too much dither. Because it is an * ordered dither, it avoids "swimming" patterns in animations with large flat sections of one color; these swimming * effects can appear in all the error-diffusion dithers here. If you can tolerate "spongy" artifacts appearing * (which look worse on small palettes), you may get very good handling of lightness by raising dither strength. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceBlueNoise (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color; float adj, strength = 32 * ditherStrength / populationBias; for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { // float pos = (PaletteReducer.thresholdMatrix64[(px & 7) | (y & 7) << 3] - 31.5f) * 0.2f + 0.5f; adj = ((PaletteReducer.TRI_BLUE_NOISE_B[(px & 63) | (y & 63) << 6] + 0.5f)); adj = adj * strength / (12f + Math.abs(adj)) + 0.5f; int rr = MathUtils.clamp((int) (adj + ((color >>> 24) )), 0, 255); adj = ((PaletteReducer.TRI_BLUE_NOISE_C[(px & 63) | (y & 63) << 6] + 0.5f)); adj = adj * strength / (12f + Math.abs(adj)) + 0.5f; int gg = MathUtils.clamp((int) (adj + ((color >>> 16) & 0xFF)), 0, 255); adj = ((PaletteReducer.TRI_BLUE_NOISE [(px & 63) | (y & 63) << 6] + 0.5f)); adj = adj * strength / (12f + Math.abs(adj)) + 0.5f; int bb = MathUtils.clamp((int) (adj + ((color >>> 8) & 0xFF)), 0, 255); pixmap.drawPixel(px, y, paletteArray[paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]); } } } pixmap.setBlending(blending); return pixmap; } /** * A white-noise-based dither; uses the colors encountered so far during dithering as a sort of state for basic * pseudo-random number generation, while also using some blue noise from a tiling texture to offset clumping. * This tends to be very rough-looking, and generally only looks good with larger palettes or with animations. It * could be a good aesthetic choice if you want a scratchy, "distressed-looking" image. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceChaoticNoise (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; double adj, strength = ditherStrength * populationBias * 1.5; long s = 0xC13FA9A902A6328FL; for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { int rr = ((color >>> 24) ); int gg = ((color >>> 16) & 0xFF); int bb = ((color >>> 8) & 0xFF); used = paletteArray[paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]; adj = ((PaletteReducer.TRI_BLUE_NOISE[(px & 63) | (y & 63) << 6] + 0.5f) * 0.007843138f); adj *= adj * adj; //// Complicated... This starts with a checkerboard of -0.5 and 0.5, times a tiny fraction. //// The next 3 lines generate 3 low-quality-random numbers based on s, which should be //// different as long as the colors encountered so far were different. The numbers can //// each be positive or negative, and are reduced to a manageable size, summed, and //// multiplied by the earlier tiny fraction. Summing 3 random values gives us a curved //// distribution, centered on about 0.0 and weighted so most results are close to 0. //// Two of the random numbers use an XLCG, and the last uses an LCG. adj += ((px + y & 1) - 0.5f) * 0x1.8p-49 * strength * (((s ^ 0x9E3779B97F4A7C15L) * 0xC6BC279692B5CC83L >> 15) + ((~s ^ 0xDB4F0B9175AE2165L) * 0xD1B54A32D192ED03L >> 15) + ((s = (s ^ rr + gg + bb) * 0xD1342543DE82EF95L + 0x91E10DA5C79E7B1DL) >> 15)); rr = Math.min(Math.max((int) (rr + (adj * ((rr - (used >>> 24))))), 0), 0xFF); gg = Math.min(Math.max((int) (gg + (adj * ((gg - (used >>> 16 & 0xFF))))), 0), 0xFF); bb = Math.min(Math.max((int) (bb + (adj * ((bb - (used >>> 8 & 0xFF))))), 0), 0xFF); pixmap.drawPixel(px, y, paletteArray[paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]); } } } pixmap.setBlending(blending); return pixmap; } /** * Modifies the given Pixmap so it only uses colors present in this PaletteReducer, using Floyd-Steinberg to dither * but modifying patterns slightly by introducing triangular-distributed blue noise. If you want to reduce the * colors in a Pixmap based on what it currently contains, call {@link #analyze(Pixmap)} with {@code pixmap} as its * argument, then call this method with the same Pixmap. You may instead want to use a known palette instead of one * computed from a Pixmap; {@link #exact(int[])} is the tool for that job. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceScatter (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; float rdiff, gdiff, bdiff; float er, eg, eb; byte paletteIndex; float w1 = ditherStrength * 3.5f, w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f; for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { float tbn = PaletteReducer.TRI_BLUE_NOISE_MULTIPLIERS[(px & 63) | ((y << 6) & 0xFC0)]; er = curErrorRed[px] * tbn; eg = curErrorGreen[px] * tbn; eb = curErrorBlue[px] * tbn; int rr = Math.min(Math.max((int)(((color >>> 24) ) + er + 0.5f), 0), 0xFF); int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + eg + 0.5f), 0), 0xFF); int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + eb + 0.5f), 0), 0xFF); paletteIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))]; used = paletteArray[paletteIndex & 0xFF]; pixmap.drawPixel(px, y, used); rdiff = (0x2.Ep-8f * ((color>>>24)- (used>>>24)) ); gdiff = (0x2.Ep-8f * ((color>>>16&255)-(used>>>16&255))); bdiff = (0x2.Ep-8f * ((color>>>8&255)- (used>>>8&255)) ); rdiff *= 1.25f / (0.25f + Math.abs(rdiff)); gdiff *= 1.25f / (0.25f + Math.abs(gdiff)); bdiff *= 1.25f / (0.25f + Math.abs(bdiff)); if(px < lineLen - 1) { curErrorRed[px+1] += rdiff * w7; curErrorGreen[px+1] += gdiff * w7; curErrorBlue[px+1] += bdiff * w7; } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += rdiff * w3; nextErrorGreen[px-1] += gdiff * w3; nextErrorBlue[px-1] += bdiff * w3; } if(px < lineLen - 1) { nextErrorRed[px+1] += rdiff * w1; nextErrorGreen[px+1] += gdiff * w1; nextErrorBlue[px+1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } } pixmap.setBlending(blending); return pixmap; } /** * An error-diffusion dither based on {@link #reduceFloydSteinberg(Pixmap)}, but adding in triangular-mapped blue * noise before diffusing, like {@link #reduceBlueNoise(Pixmap)}. This looks like {@link #reduceScatter(Pixmap)} in * many cases, but smooth gradients are much smoother with Neue than Scatter. Scatter multiplies error by a blue * noise value, where this adds blue noise regardless of error. This also preserves color better than TrueBlue, * while keeping similar gradient smoothness. The algorithm here uses a 2x2 rough checkerboard pattern to offset * some roughness that can appear in blue noise; the checkerboard can appear in some cases when a dithered image is * zoomed with certain image filters. *
* Neue is a German word for "new," and this is a new look at Scatter's technique. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceNeue(Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; float rdiff, gdiff, bdiff; float er, eg, eb; byte paletteIndex; float w1 = ditherStrength * 7f, w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f, adj, strength = (32f * ditherStrength / (populationBias * populationBias * populationBias)), limit = (float) Math.pow(80, 1.635 - populationBias); for (int py = 0; py < h; py++) { int ny = py + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, py); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, py, 0); else { adj = ((TRI_BLUE_NOISE[(px & 63) | (py & 63) << 6] + 0.5f) * 0.005f); // plus or minus 255/400 adj = Math.min(Math.max(adj * strength, -limit), limit); er = adj + (curErrorRed[px]); eg = adj + (curErrorGreen[px]); eb = adj + (curErrorBlue[px]); int rr = MathUtils.clamp((int)(((color >>> 24) ) + er + 0.5f), 0, 0xFF); int gg = MathUtils.clamp((int)(((color >>> 16) & 0xFF) + eg + 0.5f), 0, 0xFF); int bb = MathUtils.clamp((int)(((color >>> 8) & 0xFF) + eb + 0.5f), 0, 0xFF); paletteIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))]; used = paletteArray[paletteIndex & 0xFF]; pixmap.drawPixel(px, py, used); rdiff = (0x1.7p-10f * ((color>>>24)- (used>>>24)) ); gdiff = (0x1.7p-10f * ((color>>>16&255)-(used>>>16&255))); bdiff = (0x1.7p-10f * ((color>>>8&255)- (used>>>8&255)) ); rdiff *= 1.25f / (0.25f + Math.abs(rdiff)); gdiff *= 1.25f / (0.25f + Math.abs(gdiff)); bdiff *= 1.25f / (0.25f + Math.abs(bdiff)); if(px < lineLen - 1) { curErrorRed[px+1] += rdiff * w7; curErrorGreen[px+1] += gdiff * w7; curErrorBlue[px+1] += bdiff * w7; } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += rdiff * w3; nextErrorGreen[px-1] += gdiff * w3; nextErrorBlue[px-1] += bdiff * w3; } if(px < lineLen - 1) { nextErrorRed[px+1] += rdiff * w1; nextErrorGreen[px+1] += gdiff * w1; nextErrorBlue[px+1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } } pixmap.setBlending(blending); return pixmap; } /** * Given by Joel Yliluoma in a dithering article. */ static final int[] thresholdMatrix8 = { 0, 4, 2, 6, 3, 7, 1, 5, }; /** * Given by Joel Yliluoma in a dithering article. */ static final int[] thresholdMatrix16 = { 0, 12, 3, 15, 8, 4, 11, 7, 2, 14, 1, 13, 10, 6, 9, 5, }; /** * Given by Joel Yliluoma in a dithering article. */ static final int[] thresholdMatrix64 = { 0, 48, 12, 60, 3, 51, 15, 63, 32, 16, 44, 28, 35, 19, 47, 31, 8, 56, 4, 52, 11, 59, 7, 55, 40, 24, 36, 20, 43, 27, 39, 23, 2, 50, 14, 62, 1, 49, 13, 61, 34, 18, 46, 30, 33, 17, 45, 29, 10, 58, 6, 54, 9, 57, 5, 53, 42, 26, 38, 22, 41, 25, 37, 21 }; final int[] candidates = new int[32]; /** * Compares items in ints by their luma, looking up items by the indices a and b, and swaps the two given indices if * the item at a has higher luma than the item at b. This is protected rather than private because it's more likely * that this would be desirable to override than a method that uses it, like {@link #reduceKnoll(Pixmap)}. Uses * {@link #OKLAB} to look up accurate luma for the given colors in {@code ints} (that contains RGBA8888 colors * while OKLAB uses RGB555, so {@link #shrink(int)} is used to convert). * @param ints an int array than must be able to take a and b as indices; may be modified in place * @param a an index into ints * @param b an index into ints */ protected static void compareSwap(final int[] ints, final int a, final int b) { if(OKLAB[0][ints[a|16]] > OKLAB[0][ints[b|16]]) { final int t = ints[a], st = ints[a|16]; ints[a] = ints[b]; ints[a|16] = ints[b|16]; ints[b] = t; ints[b|16] = st; } } /** * Sorting network, found by http://pages.ripco.net/~jgamble/nw.html , considered the best known for length 8. * @param i8 an 8-or-more-element array that will be sorted in-place by {@link #compareSwap(int[], int, int)} */ static void sort8(final int[] i8) { compareSwap(i8, 0, 1); compareSwap(i8, 2, 3); compareSwap(i8, 0, 2); compareSwap(i8, 1, 3); compareSwap(i8, 1, 2); compareSwap(i8, 4, 5); compareSwap(i8, 6, 7); compareSwap(i8, 4, 6); compareSwap(i8, 5, 7); compareSwap(i8, 5, 6); compareSwap(i8, 0, 4); compareSwap(i8, 1, 5); compareSwap(i8, 1, 4); compareSwap(i8, 2, 6); compareSwap(i8, 3, 7); compareSwap(i8, 3, 6); compareSwap(i8, 2, 4); compareSwap(i8, 3, 5); compareSwap(i8, 3, 4); } /** * Sorting network, found by http://pages.ripco.net/~jgamble/nw.html , considered the best known for length 16. * @param i16 a 16-element array that will be sorted in-place by {@link #compareSwap(int[], int, int)} */ static void sort16(final int[] i16) { compareSwap(i16, 0, 1); compareSwap(i16, 2, 3); compareSwap(i16, 4, 5); compareSwap(i16, 6, 7); compareSwap(i16, 8, 9); compareSwap(i16, 10, 11); compareSwap(i16, 12, 13); compareSwap(i16, 14, 15); compareSwap(i16, 0, 2); compareSwap(i16, 4, 6); compareSwap(i16, 8, 10); compareSwap(i16, 12, 14); compareSwap(i16, 1, 3); compareSwap(i16, 5, 7); compareSwap(i16, 9, 11); compareSwap(i16, 13, 15); compareSwap(i16, 0, 4); compareSwap(i16, 8, 12); compareSwap(i16, 1, 5); compareSwap(i16, 9, 13); compareSwap(i16, 2, 6); compareSwap(i16, 10, 14); compareSwap(i16, 3, 7); compareSwap(i16, 11, 15); compareSwap(i16, 0, 8); compareSwap(i16, 1, 9); compareSwap(i16, 2, 10); compareSwap(i16, 3, 11); compareSwap(i16, 4, 12); compareSwap(i16, 5, 13); compareSwap(i16, 6, 14); compareSwap(i16, 7, 15); compareSwap(i16, 5, 10); compareSwap(i16, 6, 9); compareSwap(i16, 3, 12); compareSwap(i16, 13, 14); compareSwap(i16, 7, 11); compareSwap(i16, 1, 2); compareSwap(i16, 4, 8); compareSwap(i16, 1, 4); compareSwap(i16, 7, 13); compareSwap(i16, 2, 8); compareSwap(i16, 11, 14); compareSwap(i16, 2, 4); compareSwap(i16, 5, 6); compareSwap(i16, 9, 10); compareSwap(i16, 11, 13); compareSwap(i16, 3, 8); compareSwap(i16, 7, 12); compareSwap(i16, 6, 8); compareSwap(i16, 10, 12); compareSwap(i16, 3, 5); compareSwap(i16, 7, 9); compareSwap(i16, 3, 4); compareSwap(i16, 5, 6); compareSwap(i16, 7, 8); compareSwap(i16, 9, 10); compareSwap(i16, 11, 12); compareSwap(i16, 6, 7); compareSwap(i16, 8, 9); } /** * Reduces a Pixmap to the palette this knows by using Thomas Knoll's pattern dither, which is out-of-patent since * late 2019. The output this produces is very dependent on the palette and this PaletteReducer's dither strength, * which can be set with {@link #setDitherStrength(float)}. At close-up zooms, a strong grid pattern will be visible * on most dithered output (like needlepoint). The algorithm was described in detail by Joel Yliluoma in * this dithering article. Yliluoma used an 8x8 * threshold matrix because at the time 4x4 was still covered by the patent, but using 4x4 allows a much faster * sorting step (this uses a sorting network, which works well for small input sizes like 16 items). This is still * very significantly slower than the other dithers here (although {@link #reduceKnollRoberts(Pixmap)} isn't at all * fast, it still takes less than half the time this method does). *
* Using pattern dither tends to produce some of the best results for lightness-based gradients, but when viewed * close-up the "needlepoint" pattern can be jarring for images that should look natural. * @see #reduceKnollRoberts(Pixmap) An alternative that uses a similar pattern but skews it to obscure the grid * @param pixmap a Pixmap that will be modified * @return {@code pixmap}, after modifications */ public Pixmap reduceKnoll (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used, cr, cg, cb, usedIndex; final float errorMul = (ditherStrength * populationBias); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { int er = 0, eg = 0, eb = 0; cr = (color >>> 24); cg = (color >>> 16 & 0xFF); cb = (color >>> 8 & 0xFF); for (int i = 0; i < 16; i++) { int rr = Math.min(Math.max((int) (cr + er * errorMul), 0), 255); int gg = Math.min(Math.max((int) (cg + eg * errorMul), 0), 255); int bb = Math.min(Math.max((int) (cb + eb * errorMul), 0), 255); usedIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF; candidates[i | 16] = shrink(candidates[i] = used = paletteArray[usedIndex]); er += cr - (used >>> 24); eg += cg - (used >>> 16 & 0xFF); eb += cb - (used >>> 8 & 0xFF); } sort16(candidates); pixmap.drawPixel(px, y, candidates[thresholdMatrix16[((px & 3) | (y & 3) << 2)]]); } } } pixmap.setBlending(blending); return pixmap; } /** * Reduces a Pixmap to the palette this knows by using a skewed version of Thomas Knoll's pattern dither, which is * out-of-patent since late 2019, using the harmonious numbers rediscovered by Martin Roberts to handle the skew. * The output this produces is very dependent on the palette and this PaletteReducer's dither strength, which can be * set with {@link #setDitherStrength(float)}. A hexagonal pattern can be visible on many outputs this produces; * this artifact can be mitigated by lowering dither strength. The algorithm was described in detail by Joel * Yliluoma in this dithering article. Yliluoma used an * 8x8 threshold matrix because at the time 4x4 was still covered by the patent, but using 4x4 allows a much faster * sorting step (this uses a sorting network, which works well for small input sizes like 16 items). This is stil * very significantly slower than the other dithers here (except for {@link #reduceKnoll(Pixmap)}. *
* While the original Knoll pattern dither has square-shaped "needlepoint" artifacts, this has a varying-size * hexagonal or triangular pattern of dots that it uses to dither. Much like how Simplex noise uses a triangular * lattice to improve the natural feeling of noise relative to earlier Perlin noise and its square lattice, the * skew here makes the artifacts usually less-noticeable. * @see #reduceKnoll(Pixmap) An alternative that uses a similar pattern but has a more obvious grid * @param pixmap a Pixmap that will be modified * @return {@code pixmap}, after modifications */ public Pixmap reduceKnollRoberts (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used, cr, cg, cb, usedIndex; final float errorMul = ditherStrength * populationBias * 1.25f; for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if ((color & 0x80) == 0 && hasTransparent) pixmap.drawPixel(px, y, 0); else { int er = 0, eg = 0, eb = 0; cr = (color >>> 24); cg = (color >>> 16 & 0xFF); cb = (color >>> 8 & 0xFF); for (int c = 0; c < 8; c++) { int rr = Math.min(Math.max((int) (cr + er * errorMul), 0), 255); int gg = Math.min(Math.max((int) (cg + eg * errorMul), 0), 255); int bb = Math.min(Math.max((int) (cb + eb * errorMul), 0), 255); usedIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF; candidates[c | 16] = shrink(candidates[c] = used = paletteArray[usedIndex]); er += cr - (used >>> 24); eg += cg - (used >>> 16 & 0xFF); eb += cb - (used >>> 8 & 0xFF); } sort8(candidates); pixmap.drawPixel(px, y, candidates[thresholdMatrix8[ ((int) (px * 0x1.C13FA9A902A6328Fp3 + y * 0x1.9E3779B97F4A7C15p-2) & 3) ^ ((px & 3) | (y & 1) << 2) ]]); } } } pixmap.setBlending(blending); return pixmap; } /** * Retrieves a random non-0 color index for the palette this would reduce to, with a higher likelihood for colors * that are used more often in reductions (those with few similar colors). The index is returned as a byte that, * when masked with 255 as with {@code (palette.randomColorIndex(random) & 255)}, can be used as an index into a * palette array with 256 or less elements that should have been used with {@link #exact(int[])} before to set the * palette this uses. * @param random a Random instance, which may be seeded * @return a randomly selected color index from this palette with a non-uniform distribution, can be any byte but 0 */ public byte randomColorIndex(Random random) { return paletteMapping[random.nextInt() >>> 17]; } /** * Retrieves a random non-transparent color from the palette this would reduce to, with a higher likelihood for * colors that are used more often in reductions (those with few similar colors). The color is returned as an * RGBA8888 int; you can assign one of these into a Color with {@link Color#rgba8888ToColor(Color, int)} or * {@link Color#set(int)}. * @param random a Random instance, which may be seeded * @return a randomly selected RGBA8888 color from this palette with a non-uniform distribution */ public int randomColor(Random random) { return paletteArray[paletteMapping[random.nextInt() >>> 17] & 255]; } /** * Looks up {@code color} as if it was part of an image being color-reduced and finds the closest color to it in the * palette this holds. Both the parameter and the returned color are RGBA8888 ints. * @param color an RGBA8888 int that represents a color this should try to find a similar color for in its palette * @return an RGBA8888 int representing a color from this palette, or 0 if color is mostly transparent * (0 is often but not always in the palette) */ public int reduceSingle(int color) { if((color & 0x80) == 0) // less visible than half-transparent return 0; // transparent return paletteArray[paletteMapping[ (color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] & 0xFF]; } /** * Looks up {@code color} as if it was part of an image being color-reduced and finds the closest color to it in the * palette this holds. The parameter is a RGBA8888 int, the returned color is a byte index into the * {@link #paletteArray} (mask it like: {@code paletteArray[reduceIndex(color) & 0xFF]}). * @param color an RGBA8888 int that represents a color this should try to find a similar color for in its palette * @return a byte index that can be used to look up a color from the {@link #paletteArray} */ public byte reduceIndex(int color) { if((color & 0x80) == 0) // less visible than half-transparent return 0; // transparent return paletteMapping[ (color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)]; } /** * Looks up {@code color} as if it was part of an image being color-reduced and finds the closest color to it in the * palette this holds. Both the parameter and the returned color are packed float colors, as produced by * {@link Color#toFloatBits()} or many methods in SColor. * @param packedColor a packed float color this should try to find a similar color for in its palette * @return a packed float color from this palette, or 0f if color is mostly transparent * (0f is often but not always in the palette) */ public float reduceFloat(float packedColor) { final int color = NumberUtils.floatToIntBits(packedColor); if(color >= 0) // if color is non-negative, then alpha is less than half of opaque return 0f; return NumberUtils.intBitsToFloat(Integer.reverseBytes(paletteArray[paletteMapping[ (color << 7 & 0x7C00) | (color >>> 6 & 0x3E0) | (color >>> 19)] & 0xFF] & 0xFFFFFFFE)); } /** * Modifies {@code color} so its RGB values will match the closest color in this PaletteReducer's palette. If color * has {@link Color#a} less than 0.5f, this will simply set color to be fully transparent, with rgba all 0. * @param color a libGDX Color that will be modified in-place; do not use a Color constant, use {@link Color#cpy()} * or a temporary Color * @return color, after modifications. */ public Color reduceInPlace(Color color) { if(color.a < 0.5f) return color.set(0); return color.set(paletteArray[paletteMapping[ ((int) (color.r * 0x1f.8p+10) & 0x7C00) | ((int) (color.g * 0x1f.8p+5) & 0x3E0) | ((int) (color.r * 0x1f.8p+0))] & 0xFF]); } /** * Edits this PaletteReducer by changing each used color in the Oklab color space with an {@link Interpolation}. * This allows adjusting lightness, such as for gamma correction. You could use {@link Interpolation#pow2InInverse} * to use the square root of a color's lightness instead of its actual lightness, or {@link Interpolation#pow2In} to * square the lightness instead. * @param lightness an Interpolation that will affect the lightness of each color * @return this PaletteReducer, for chaining */ public PaletteReducer alterColorsLightness(Interpolation lightness) { int[] palette = paletteArray; for (int idx = 0; idx < colorCount; idx++) { int s = shrink(palette[idx]); palette[idx] = oklabToRGB(lightness.apply(OKLAB[0][s]), OKLAB[1][s], OKLAB[2][s], (palette[idx] & 0xFE) / 254f); } return this; } /** * Edits this PaletteReducer by changing each used color in the Oklab color space with an {@link Interpolation}. * This allows adjusting lightness, such as for gamma correction, but also individually emphasizing or * de-emphasizing different aspects of the chroma. You could use {@link Interpolation#pow2InInverse} to use the * square root of a color's lightness instead of its actual lightness (which, because lightness is in the 0 to 1 * range, always results in a color with the same lightness or a higher lightness), or {@link Interpolation#pow2In} * to square the lightness instead (this always results in a color with the same or lower lightness). You could make * colors more saturated by passing {@link Interpolation#circle} to greenToRed and blueToYellow, or get a * less-extreme version by using {@link Interpolation#smooth}. To desaturate colors is a different task; you can * create a {@link OtherMath.BiasGain} Interpolation with 0.5 turning and maybe 0.25 to 0.75 shape to produce * different strengths of desaturation. Using a shape of 1.5 to 4 with BiasGain is another way to saturate the * colors. * @param lightness an Interpolation that will affect the lightness of each color * @param greenToRed an Interpolation that will make colors more green if it evaluates below 0.5 or more red otherwise * @param blueToYellow an Interpolation that will make colors more blue if it evaluates below 0.5 or more yellow otherwise * @return this PaletteReducer, for chaining */ public PaletteReducer alterColorsOklab(Interpolation lightness, Interpolation greenToRed, Interpolation blueToYellow) { int[] palette = paletteArray; for (int idx = 0; idx < colorCount; idx++) { int s = shrink(palette[idx]); float L = lightness.apply(OKLAB[0][s]); float A = greenToRed.apply(-1, 1, OKLAB[1][s] * 0.5f + 0.5f); float B = blueToYellow.apply(-1, 1, OKLAB[2][s] * 0.5f + 0.5f); palette[idx] = oklabToRGB(L, A, B, (palette[idx] & 0xFE) / 254f); } return this; } /** * Edits this PaletteReducer by changing each used color so lighter colors lean towards warmer hues, while darker * colors lean toward cooler or more purple-ish hues. * @return this PaletteReducer, for chaining */ public PaletteReducer hueShift() { int[] palette = paletteArray; for (int idx = 0; idx < colorCount; idx++) { int s = shrink(palette[idx]); float L = OKLAB[0][s]; float A = OKLAB[1][s] + (L - 0.5f) * 0.04f; float B = OKLAB[2][s] + (L - 0.5f) * 0.08f; palette[idx] = oklabToRGB(L, A, B, (palette[idx] & 0xFE) / 254f); } return this; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy