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

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

The 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.Gdx;
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_SNUGGLY;

/**
 * Data that can be used to limit the colors present in a Pixmap or other image, here with the goal of using 256 or fewer
 * 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 { /** * A "geometric" palette generated by choosing quasi-random points that lay inside the Oklab gamut, and equalizing * the sizes of the "cells" around each color in Oklab space. Each "cell" is composed of the colors that are closest * to that particular cell's color, and having them be equal-size means there should be comparable representation * for any given hue/saturation/lightness combination. *
* This is currently the default. *
* While you can modify the individual items in this array, this is discouraged, because various constructors and * methods in this class use SNUGGLY 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[] SNUGGLY = { 0x00000000, 0x000000FF, 0x111111FF, 0x20090DFF, 0x222222FF, 0x152C14FF, 0x333333FF, 0x444444FF, 0x555555FF, 0x5C6365FF, 0x666666FF, 0x777777FF, 0x888888FF, 0x999999FF, 0xAAAAAAFF, 0xBBBBBBFF, 0xC5BFBDFF, 0xCCCCCCFF, 0xDDDDDDFF, 0xEEEEEEFF, 0xFFFFFFFF, 0x9F1A1AFF, 0xF62923FF, 0xF57266FF, 0xA9574AFF, 0xD14127FF, 0xF5927CFF, 0x923C26FF, 0xF7582BFF, 0xAF3E1DFF, 0xDD5926FF, 0xBD7152FF, 0xB38A73FF, 0xBB5A22FF, 0xD68B5FFF, 0xD1A991FF, 0xF2782BFF, 0x76401BFF, 0x7E5B42FF, 0x583317FF, 0xF1B384FF, 0xCF7627FF, 0xEECAA6FF, 0xF49432FF, 0x94591CFF, 0x3D2811FF, 0x947653FF, 0xA67220FF, 0xD8AA61FF, 0xF4AF38FF, 0xD09329FF, 0xAD8C28FF, 0x67551DFF, 0xF0C63DFF, 0xB1A473FF, 0xD4B42EFF, 0xB6A934FF, 0x78712EFF, 0xF0E241FF, 0xE2DB7FFF, 0xE9E5AEFF, 0x96945EFF, 0x878628FF, 0xD2D934FF, 0x484C1FFF, 0xE8FA37FF, 0xC2CA82FF, 0xE9F884FF, 0xB8CF39FF, 0xA9C033FF, 0x90A62DFF, 0x576A24FF, 0xC0F242FF, 0xE8FAC2FF, 0x709C30FF, 0x5C8326FF, 0x7DBC3EFF, 0x89E437FF, 0x8FFA37FF, 0x78D031FF, 0x96BE7FFF, 0x9FE680FF, 0x1E4216FF, 0x4AB53EFF, 0xA6F99DFF, 0x225B1DFF, 0x257420FF, 0x359B2FFF, 0x34C62CFF, 0x2CAC25FF, 0x3CF735FF, 0x689067FF, 0x36DF32FF, 0x238B24FF, 0x7BD77CFF, 0x77AC7AFF, 0x51FA7BFF, 0xA7E5BEFF, 0x36D676FF, 0x3BEB8CFF, 0x8AD6AFFF, 0x36BD78FF, 0x2C8D5CFF, 0x31A56EFF, 0x2A7756FF, 0x49F9BAFF, 0x296751FF, 0x3CCBA4FF, 0xACF8E4FF, 0x3ADCBFFF, 0x3C7E73FF, 0x21574EFF, 0x79BBB3FF, 0x19433FFF, 0x58F9ECFF, 0x36B4AAFF, 0x3FEBE3FF, 0x49A19EFF, 0x299495FF, 0x64D8EAFF, 0x33BDD8FF, 0x3ACDEAFF, 0x277A95FF, 0xA4DBEFFF, 0x2DA4D1FF, 0x26637EFF, 0x5A859CFF, 0x2A8BC4FF, 0x39ACF2FF, 0x1E4B6CFF, 0x73B4EAFF, 0x6898C1FF, 0x93C0E8FF, 0x2D8EF0FF, 0x3077C7FF, 0x3F6BA2FF, 0x23589DFF, 0x16355EFF, 0x2164C3FF, 0x2F70EFFF, 0x6791F0FF, 0x20418DFF, 0x2559EEFF, 0x909FCAFF, 0x18254FFF, 0x2147C3FF, 0x677DC9FF, 0x1D319FFF, 0x233DEFFF, 0x5057CAFF, 0x686FEFFF, 0x14178BFF, 0x9698F1FF, 0x101060FF, 0x0D0D31FF, 0x1717BBFF, 0x1E1BEBFF, 0x4A469DFF, 0x4C4971FF, 0x5042EDFF, 0x6F6B98FF, 0x8983AEFF, 0x3C1DCBFF, 0xC5BBEEFF, 0x725FBFFF, 0x4E33B7FF, 0x704FEEFF, 0x311A7EFF, 0x625490FF, 0x9577F0FF, 0x521CEDFF, 0xB9A3F0FF, 0x4215A4FF, 0x3E305AFF, 0x9F84D2FF, 0x6E3FBEFF, 0x7522EDFF, 0x50307EFF, 0x9168CAFF, 0x2E1057FF, 0x9753F0FF, 0x6A18C5FF, 0x5E1797FF, 0x9624F0FF, 0x8E46C4FF, 0xC37CF0FF, 0x4D126CFF, 0x71378FFF, 0xBD58F1FF, 0x921CCAFF, 0xB523ECFF, 0x8019A5FF, 0xB347C9FF, 0x85528EFF, 0x2B0E30FF, 0xB76AC4FF, 0xD825EEFF, 0xAC1CB7FF, 0x996B9CFF, 0xCD8BD0FF, 0x701674FF, 0xE758EDFF, 0x963798FF, 0xEEA4ECFF, 0xCC1FC7FF, 0xEF89ECFF, 0xF52AEEFF, 0xA550A2FF, 0xD74BCBFF, 0xEEC0EAFF, 0xF26FE5FF, 0x921684FF, 0xC9A2C3FF, 0xF2D5EDFF, 0x6E3A65FF, 0x46133CFF, 0xF221C2FF, 0xB92095FF, 0xB588A9FF, 0xD821A0FF, 0xD470B4FF, 0x621449FF, 0xCA50A2FF, 0xF450B8FF, 0xA7196DFF, 0x7F1753FF, 0x913668FF, 0xF62691FF, 0xC91871FF, 0xBE3E7AFF, 0xA45275FF, 0xE42173FF, 0x582D3FFF, 0xF28FB8FF, 0xDB5286FF, 0xF473A3FF, 0x7E4D5DFF, 0xB31848FF, 0xB27185FF, 0x911A3EFF, 0xF75480FF, 0xF62459FF, 0xD5214DFF, 0xF0ACBBFF, 0x8F656DFF, 0xAC3C4FFF, 0xC85461FF, 0xD58D95FF, 0x73353AFF, 0x5E1318FF, 0x3E1013FF, 0xD77076FF, 0xEE4B52FF, 0xE01A21FF, 0xC11B1FFF, 0x7C181AFF, }; /** * 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 was the default for many releases, and had 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. Aurora still looks good a lot of the time, but it has trouble with * shades of yellow and with light green, at least. It was replaced with the default Snuggly palette in 0.4.4 . *
* 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, }; /** * An expanded palette with 1024 colors instead of {@link #SNUGGLY}'s 256. This includes fully-transparent black * as its first item, but has no other transparent or partially transparent colors. This is meant to be used by * {@link #analyzeReductive(Pixmap, double, int)} as a basis that can have colors removed if they don't match often * in a source image. */ public static final int[] BIG_PALETTE = new int[]{ // 0x00000000, 0x15111BFF, 0x271C1DFF, 0x212230FF, 0x372B26FF, 0x5B464EFF, 0x4F4F47FF, 0x435257FF, // 0x4F5155FF, 0x5C5055FF, 0x654F50FF, 0x525754FF, 0x465F5FFF, 0x6F565BFF, 0x4A6469FF, 0x5B6276FF, // 0x686260FF, 0x55686AFF, 0x616C70FF, 0x786371FF, 0x607577FF, 0x6C7B6FFF, 0x72798CFF, 0x827588FF, // 0x6E8174FF, 0x8F7678FF, 0x887D7EFF, 0x89807DFF, 0x8A8475FF, 0x848A75FF, 0x9A8887FF, 0x7B948EFF, // 0x918F8BFF, 0xA48888FF, 0xA48886FF, 0x979284FF, 0x8C949FFF, 0x9A9CA3FF, 0x9C9F9BFF, 0x9FA1A5FF, // 0xA99FA1FF, 0xA19FB9FF, 0xB99C93FF, 0x92AAACFF, 0x94AAB7FF, 0x99A9BCFF, 0xB4A4ABFF, 0xABADBEFF, // 0xBCA9B7FF, 0xBAACAFFF, 0xADBCA6FF, 0xC6B5A5FF, 0xB6BBBAFF, 0xD2BAB4FF, 0xC5BFD8FF, 0xC1CAB5FF, // 0xB4CFC5FF, 0xC2E2E8FF, 0xE5DCEEFF, 0xECE5E0FF, 0xFFEDD4FF, 0xF6F2D6FF, 0xFD3684FF, 0xEA6791FF, // 0xBF8494FF, 0xA27E87FF, 0x9B0C4BFF, 0xE84E81FF, 0xF02E78FF, 0xC96682FF, 0xE989A2FF, 0xD5376FFF, // 0xC22B62FF, 0x532B36FF, 0xEDA6B7FF, 0x691433FF, 0xC4115BFF, 0xBE5B76FF, 0x925262FF, 0x72444FFF, // 0x4A1D29FF, 0xC75A76FF, 0xEF6B8CFF, 0xFB5D88FF, 0xE74876FF, 0xAA3155FF, 0xF3266CFF, 0x6A2B3BFF, // 0xFE91A8FF, 0xA36B76FF, 0xA9485FFF, 0xF7CAD1FF, 0xAC5567FF, 0xC1838DFF, 0xFF7B94FF, 0xEB8193FF, // 0xD5939DFF, 0xE56B82FF, 0x7A5056FF, 0xCC8D95FF, 0xDF8E99FF, 0xFB7189FF, 0xBA2449FF, 0xA80B3CFF, // 0x891633FF, 0x380D16FF, 0xCF737EFF, 0x782031FF, 0xAD898CFF, 0xDD9BA1FF, 0xE89FA5FF, 0xC38E92FF, // 0x563437FF, 0xF26476FF, 0x86172FFF, 0x913643FF, 0xCB878CFF, 0xB44C58FF, 0xA65C62FF, 0xCE6B74FF, // 0xA85F65FF, 0xFB6E7DFF, 0xB05059FF, 0xCB3C50FF, 0xBF8184FF, 0xB83748FF, 0xDF5A67FF, 0xE66671FF, // 0x884146FF, 0x9B2637FF, 0x9D5156FF, 0xE68C90FF, 0xEA3A52FF, 0x9C6667FF, 0x93323BFF, 0xE04A57FF, // 0xDC4452FF, 0xE74E5AFF, 0xC91738FF, 0xDC3F4EFF, 0xA77E7EFF, 0x7B1C25FF, 0xD68988FF, 0xA03C3EFF, // 0x6B4B4AFF, 0xA95D5BFF, 0xDF6363FF, 0xB06D6AFF, 0xE68682FF, 0xA1706DFF, 0xCA6662FF, 0x71423FFF, // 0xBF6E6AFF, 0xEBC2BFFF, 0x743230FF, 0xE03D40FF, 0xCF817BFF, 0xBD0E22FF, 0x714340FF, 0xDB3538FF, // 0x7E3734FF, 0xE79B94FF, 0x93635EFF, 0xA26963FF, 0x835A56FF, 0xBC322FFF, 0x89615CFF, 0xC6524BFF, // 0xC83C37FF, 0xF8B7AFFF, 0xE6493FFF, 0xEB7E70FF, 0x935B53FF, 0xF41C0EFF, 0xA43D2FFF, 0xFF725DFF, // 0xCA9186FF, 0xC34B38FF, 0xEF6B55FF, 0xF87961FF, 0xEB7D65FF, 0xDCA699FF, 0xC37C6BFF, 0xD26349FF, // 0xE65432FF, 0xD4573AFF, 0xDD5332FF, 0xD6765DFF, 0xEBB5A6FF, 0xD14924FF, 0xDF6C4CFF, 0xD88269FF, // 0x562F24FF, 0xF95322FF, 0xD37C62FF, 0x8D3316FF, 0xE5480BFF, 0xA8634CFF, 0xD2A291FF, 0xB65838FF, // 0xD1795AFF, 0x935540FF, 0xEF500BFF, 0x7B3013FF, 0x88503CFF, 0xB3715AFF, 0xDF7F5DFF, 0xAD502EFF, // 0xFEB89FFF, 0xE09273FF, 0xDD997EFF, 0x943F1BFF, 0xBC470BFF, 0xC3795AFF, 0xBF7352FF, 0xAB6040FF, // 0xAB755EFF, 0xD6784FFF, 0xE17A4CFF, 0x8D4C2FFF, 0x6C3B23FF, 0xE2AD95FF, 0xAC9082FF, 0x95481DFF, // 0xE77D44FF, 0xE48C5FFF, 0x452A1CFF, 0xA36A4CFF, 0xF87C34FF, 0xF8CBB2FF, 0xE0956DFF, 0xF9843FFF, // 0xAB5A28FF, 0x954509FF, 0xDFA07BFF, 0xAE6030FF, 0xED7721FF, 0x925C39FF, 0xE58341FF, 0xE67F37FF, // 0xBA5A05FF, 0xEF7D27FF, 0xB58A6EFF, 0xCA8250FF, 0x9A765EFF, 0xEFCBB3FF, 0xB38464FF, 0xF7BB92FF, // 0x907969FF, 0xB0815FFF, 0xB09078FF, 0xAB8569FF, 0xB98862FF, 0x9F5A1DFF, 0xFDB986FF, 0x865328FF, // 0xC58654FF, 0xFBA45FFF, 0xE9B58CFF, 0x8E6646FF, 0xBB7C47FF, 0x915003FF, 0xA88567FF, 0xCA7E33FF, // 0xBF7422FF, 0xCF904FFF, 0x8F5B1FFF, 0x5C4936FF, 0xB68D61FF, 0xFF9C19FF, 0xF69814FF, 0xE89323FF, // 0xEB9930FF, 0xAE7834FF, 0x513714FF, 0xB8823BFF, 0xD7B993FF, 0xD88C12FF, 0xC29B65FF, 0xB37A1EFF, // 0xC3A67DFF, 0xC5A26EFF, 0xB38643FF, 0xDFA957FF, 0xA69274FF, 0xFBDBA9FF, 0x7E5815FF, 0xB98939FF, // 0xE1AD56FF, 0x9D7A3EFF, 0xB69153FF, 0xBF9445FF, 0x60491BFF, 0xEBAD13FF, 0xEFB632FF, 0x95752DFF, // 0xF7DFAAFF, 0x887445FF, 0x84661EFF, 0x6E6550FF, 0xE0AF2DFF, 0xEFBA1FFF, 0xE2D3ACFF, 0x856C28FF, // 0x8D700BFF, 0xE8CD7CFF, 0xAC9753FF, 0x7B6515FF, 0x827239FF, 0x4F4213FF, 0xDAB945FF, 0xEDCD5FFF, // 0x856F15FF, 0x504725FF, 0xC8C1A0FF, 0xF8D957FF, 0x978A4FFF, 0x85742AFF, 0xC3B984FF, 0xDAC13BFF, // 0xB8AD70FF, 0xC6AF2AFF, 0x726D4AFF, 0xA1953FFF, 0x969373FF, 0xF6F2CEFF, 0x878049FF, 0x8C8756FF, // 0xC9BD44FF, 0xCBC57FFF, 0x666444FF, 0x827F56FF, 0x48451FFF, 0xBCB65EFF, 0x3D3C16FF, 0xDADA9AFF, // 0x4A4808FF, 0xB4B241FF, 0xA6A41AFF, 0xBBBD52FF, 0x9C9E3DFF, 0x6B6E26FF, 0x888C4FFF, 0xC8D072FF, // 0x474B09FF, 0xE1E9A2FF, 0x979F1AFF, 0x5D5F49FF, 0x676E36FF, 0xB0BC80FF, 0x121404FF, 0x9BAE57FF, // 0x6A7B13FF, 0xACB887FF, 0x8B985EFF, 0xDDF596FF, 0x4B5A11FF, 0x97A671FF, 0x688003FF, 0xBBD18AFF, // 0x99B15EFF, 0x89A73BFF, 0xBBD976FF, 0x6A851EFF, 0x97B35FFF, 0x78904BFF, 0x718E41FF, 0x4E6820FF, // 0xC7DBADFF, 0x9FC071FF, 0x849173FF, 0x93C45FFF, 0xAFE873FF, 0x80B743FF, 0x91DE2EFF, 0x83A067FF, // 0x4D7718FF, 0x9EFD14FF, 0x95B974FF, 0x536C40FF, 0x8AC856FF, 0x8FAC7BFF, 0xA1D17DFF, 0x70B536FF, // 0x81E92BFF, 0x69BB33FF, 0x557A41FF, 0x99B988FF, 0xB2D0A3FF, 0x71B54CFF, 0x355525FF, 0x7FC75CFF, // 0x73BB50FF, 0xACEB90FF, 0x426630FF, 0x6BDE24FF, 0x87A47CFF, 0x80BA6CFF, 0x4A783BFF, 0x5B814FFF, // 0x51A437FF, 0x63BF48FF, 0x317F1AFF, 0x70E253FF, 0x47DD0CFF, 0x89C67BFF, 0x8FFB7BFF, 0x41633BFF, // 0x597A54FF, 0x5AC24CFF, 0x4DB23FFF, 0x90CF87FF, 0x49A640FF, 0x7EA579FF, 0x6D9169FF, 0x6FC66AFF, // 0x2E9E2BFF, 0x56C252FF, 0x214020FF, 0x78E175FF, 0x90D98EFF, 0x18691DFF, 0x38AF3DFF, 0x3B673BFF, // 0x259B2FFF, 0x3E8340FF, 0x9AF09BFF, 0x72A372FF, 0x587758FF, 0xA5E5A6FF, 0x519859FF, 0x9AB79CFF, // 0x1F8D38FF, 0x50C865FF, 0x60E177FF, 0x439451FF, 0x3C8349FF, 0x51A55FFF, 0x62966AFF, 0x9EC7A4FF, // 0x85E298FF, 0x6BEB8BFF, 0x64A973FF, 0x66B979FF, 0x112D19FF, 0x76A17FFF, 0xD4FFDDFF, 0x37844FFF, // 0x509363FF, 0x41E67DFF, 0xADECBDFF, 0x0FD068FF, 0x26AE5DFF, 0x6FB282FF, 0x536A59FF, 0x98C9A6FF, // 0x6EA980FF, 0x62A377FF, 0x7A9C85FF, 0x7FD99EFF, 0x45644FFF, 0x31DE86FF, 0x5B7E67FF, 0x86BB9AFF, // 0x63A27CFF, 0x7FD2A1FF, 0x91BBA1FF, 0x15452DFF, 0x8BE4B1FF, 0x6C8F7AFF, 0x5EAC85FF, 0x70AC8CFF, // 0x4C7A62FF, 0x64EBABFF, 0x67C99AFF, 0x9BCFB4FF, 0x71AF90FF, 0x7DD4ABFF, 0x5CB78EFF, 0x90AE9FFF, // 0x1AB17BFF, 0x61E1AAFF, 0x5EAF8BFF, 0x53D7A0FF, 0x4FA585FF, 0x5D8C7AFF, 0x49F0BDFF, 0x497363FF, // 0x427765FF, 0x15B78EFF, 0x7EA799FF, 0x317F6BFF, 0x42D2B1FF, 0x729389FF, 0x7BB0A2FF, 0x77AC9FFF, // 0x76A79BFF, 0x0E6C5CFF, 0x25AD95FF, 0x38BDA6FF, 0x7FBDB0FF, 0x8BAEA7FF, 0x478479FF, 0xA9CFC7FF, // 0x55B5A5FF, 0x8ABAB2FF, 0x35FDE8FF, 0x50928AFF, 0x0CCFBFFF, 0x3B615CFF, 0x3CD9CAFF, 0x54C1B6FF, // 0x6A928FFF, 0x5FE2DAFF, 0x558A86FF, 0xA0D7D4FF, 0x33D7D1FF, 0x4E7C79FF, 0x0D7976FF, 0x458F8CFF, // 0x139391FF, 0x7ED3D3FF, 0x0B7778FF, 0x609A9BFF, 0x48BDC0FF, 0x177679FF, 0x68B2B5FF, 0x439EA2FF, // 0x75C9CFFF, 0x69BAC1FF, 0x5C8287FF, 0x186B74FF, 0x3F787FFF, 0x67ECFFFF, 0x35757EFF, 0x56C4D4FF, // 0x74B9C4FF, 0x153338FF, 0x43DFFAFF, 0x93C8D3FF, 0x18CDEBFF, 0x38808EFF, 0x47808DFF, 0x5D9BA9FF, // 0x85A9B2FF, 0x4A8896FF, 0x396A76FF, 0x88BDCBFF, 0x48D9FCFF, 0x65B7CDFF, 0x37D6FFFF, 0x779EAAFF, // 0x6695A4FF, 0x6DCBEAFF, 0x8CE0FEFF, 0x22C9F9FF, 0x2FA6CBFF, 0x59889AFF, 0x034557FF, 0x42A8CBFF, // 0x84DBFCFF, 0x2F596AFF, 0x75AEC4FF, 0x55A5C6FF, 0x2FB9EDFF, 0xB7E4F8FF, 0x4390B1FF, 0x1DABE0FF, // 0x548196FF, 0x63ACCFFF, 0x64B3DAFF, 0x89C2E1FF, 0x6AB4DBFF, 0x9CC9E2FF, 0x30B9FBFF, 0x63A0C1FF, // 0x4089B3FF, 0x4DAEE5FF, 0x599CC5FF, 0x0A6B9BFF, 0x348BBFFF, 0x365F79FF, 0x2D79A8FF, 0x4EB2F1FF, // 0x4D92BEFF, 0x66C2FFFF, 0x28536FFF, 0x4B9ACFFF, 0x84A7C1FF, 0x71A4C8FF, 0x478DBEFF, 0x52ACEBFF, // 0x2793D9FF, 0x86A2B7FF, 0x94CBF4FF, 0x105D8DFF, 0x2C90D5FF, 0x3178ABFF, 0x5DB2F5FF, 0x154F7AFF, // 0x5894C8FF, 0x649ECFFF, 0x559BD7FF, 0x5492C8FF, 0x143E62FF, 0x4E92D0FF, 0x4C6E8FFF, 0x3B8AD3FF, // 0x3B4E62FF, 0x70ABE7FF, 0x67ACF5FF, 0x4097F9FF, 0x8095ADFF, 0x4C77A9FF, 0x5696E2FF, 0x223245FF, // 0x7EB2F1FF, 0x92B3DBFF, 0x4778B4FF, 0x6FA2E1FF, 0xAFCBEFFF, 0x4F81C1FF, 0x175BADFF, 0x5A9BF5FF, // 0x45628AFF, 0x576C89FF, 0x4C84D7FF, 0x3078E8FF, 0x2C3F5DFF, 0x354F79FF, 0x5A84CCFF, 0x4483FAFF, // 0x4881ECFF, 0x687CA0FF, 0x5A7AB7FF, 0x1540A5FF, 0x0436B2FF, 0x6591EFFF, 0x8CB0FBFF, 0x5F80C7FF, // 0x4867ACFF, 0x90B2FAFF, 0x3B68E2FF, 0x6286E1FF, 0xC9DAFFFF, 0x496BDBFF, 0x6F8EE6FF, 0xABBBE4FF, // 0x7690DBFF, 0x7895ECFF, 0x667BBAFF, 0x6887F2FF, 0x91A1D1FF, 0x4863ECFF, 0x273588FF, 0x2A3354FF, // 0x5368BFFF, 0x919EC9FF, 0x4254D2FF, 0x4F63D9FF, 0x5165EEFF, 0x687FF7FF, 0x161163FF, 0x3C46BBFF, // 0x8996C8FF, 0x2D3294FF, 0x4B58CDFF, 0x4C56E1FF, 0x5F6DBCFF, 0x343B90FF, 0x6672BFFF, 0x352BBBFF, // 0x2C21A1FF, 0x6D79CCFF, 0x727BB4FF, 0x2C199EFF, 0x4649B8FF, 0x3F3DB8FF, 0x767EFDFF, 0x3D4186FF, // 0x989FC7FF, 0x371EB9FF, 0x8F96C6FF, 0x8A92F7FF, 0x7A81F5FF, 0x878ED3FF, 0x5546F6FF, 0x5C55E9FF, // 0x7171E1FF, 0x5949E9FF, 0x656784FF, 0x6861E7FF, 0x636498FF, 0x605DC0FF, 0xC0C2E3FF, 0x7273AFFF, // 0x7C77F1FF, 0x7875CDFF, 0x535378FF, 0x9490F1FF, 0x807EBAFF, 0x6158B1FF, 0x8281A4FF, 0x65647DFF, // 0x5F55AEFF, 0x8573F9FF, 0x641FFFFF, 0x8782BDFF, 0x534A8BFF, 0x9382F7FF, 0x5C5295FF, 0x6E51D9FF, // 0x49289FFF, 0x6F6A93FF, 0x33027BFF, 0xACA5E0FF, 0x968BD4FF, 0x8476C2FF, 0x8F74F0FF, 0x5F5C72FF, // 0x2D293BFF, 0x7659CEFF, 0x9F82FCFF, 0x726995FF, 0x2A0D59FF, 0x8B7AC5FF, 0x7E54E0FF, 0x793AEBFF, // 0x81799FFF, 0x7A51D6FF, 0xB3A1EDFF, 0x8267C9FF, 0x7653C7FF, 0xA289E9FF, 0x573995FF, 0x6B5D93FF, // 0x5B488AFF, 0x9560FAFF, 0x8174A3FF, 0xB5A7DDFF, 0x946AEAFF, 0x918AA6FF, 0x221639FF, 0x6B49ACFF, // 0x6938B9FF, 0xA679FFFF, 0x9E69FFFF, 0x48287BFF, 0xA189D8FF, 0x7731D6FF, 0xB9AADDFF, 0x624A8FFF, // 0x312843FF, 0x4A2F75FF, 0x8840EBFF, 0x60379EFF, 0x67548AFF, 0x4F118EFF, 0x726689FF, 0x9171C7FF, // 0x9883BEFF, 0x9B59F3FF, 0x866FADFF, 0x8A42E0FF, 0x9067CCFF, 0x998EADFF, 0xAD91D6FF, 0x634589FF, // 0x8643D0FF, 0xB699E0FF, 0xA878E5FF, 0x8F76B2FF, 0xAC9FBFFF, 0x8F5DCAFF, 0xC09AEFFF, 0x8C55C9FF, // 0x6A2DA5FF, 0xA186C2FF, 0x9452D7FF, 0x9A76BFFF, 0xAE66F0FF, 0xB189D9FF, 0x592F7CFF, 0x630E9AFF, // 0x651C98FF, 0xA72EFCFF, 0x7620B2FF, 0x8A51BAFF, 0x8B58B6FF, 0xBC72F7FF, 0x9E38E4FF, 0x372743FF, // 0xA42EE8FF, 0x7C519AFF, 0x6F1F9CFF, 0xAA6AD4FF, 0x533A63FF, 0x5D3276FF, 0x3B204BFF, 0x956EADFF, // 0x46384FFF, 0xB261E1FF, 0xBA8ED4FF, 0xA873C6FF, 0x703F8CFF, 0x8E4DB2FF, 0xA065C1FF, 0xC084E0FF, // 0x8447A2FF, 0x6A447DFF, 0xBA65E2FF, 0xAF5ED5FF, 0x866A93FF, 0xAE7AC6FF, 0xB0A0B7FF, 0xCF6EF4FF, // 0xB53FE0FF, 0x76418AFF, 0xAE40D5FF, 0xC153E9FF, 0xAE53D0FF, 0xB428E2FF, 0x8123A0FF, 0x593A63FF, // 0xB878CCFF, 0x925EA2FF, 0xD78DEDFF, 0xB07FBEFF, 0xC663E3FF, 0x9A59ADFF, 0xA963BDFF, 0xD369F0FF, // 0x9633AFFF, 0xA553BAFF, 0xB75ECDFF, 0x6A4174FF, 0xB588C0FF, 0xC7A2CFFF, 0x9C2BB4FF, 0xC5AACAFF, // 0x9027A6FF, 0x9D40B0FF, 0xC436DFFF, 0x9949A8FF, 0x9651A3FF, 0xD281E0FF, 0xDA3BF6FF, 0x95639DFF, // 0x793F83FF, 0x892998FF, 0xA15EACFF, 0xC8A0CDFF, 0xD51CEEFF, 0x644867FF, 0x946F97FF, 0xA972AEFF, // 0xE625FBFF, 0x9E2EA6FF, 0xC5A2C6FF, 0x721778FF, 0xA951ACFF, 0x912794FF, 0x8A6289FF, 0x6F3170FF, // 0xC291C0FF, 0xDF3AE2FF, 0xE15DE2FF, 0x974495FF, 0xCE83CAFF, 0x7D257CFF, 0xFBCBF8FF, 0xBF5ABCFF, // 0x922E91FF, 0xA351A0FF, 0xBF40BCFF, 0xFB61F6FF, 0xD82DD5FF, 0xDD92D8FF, 0xE05ADAFF, 0xB276ADFF, // 0xC76DC1FF, 0xAD5CA7FF, 0x9D6098FF, 0x9D2B98FF, 0xC346BCFF, 0xB95CB1FF, 0x85257FFF, 0x712C6CFF, // 0xA82AA0FF, 0xEA23DBFF, 0xA85F9EFF, 0xEA95DEFF, 0xC43FB7FF, 0xB159A5FF, 0x78226EFF, 0x7A0470FF, // 0xCC86C0FF, 0x7A0E70FF, 0xC822B6FF, 0xE82AD1FF, 0x755A6FFF, 0xB246A2FF, 0xEA7BD7FF, 0x5B2F53FF, // 0xBA65ABFF, 0xA34193FF, 0xAF6EA2FF, 0x930C82FF, 0x704C68FF, 0xC619AFFF, 0xCB64B7FF, 0x8B347CFF, // 0x621256FF, 0x87587DFF, 0xBB7DADFF, 0xC2A2B9FF, 0xFAA1E5FF, 0x9B3888FF, 0xF59CDFFF, 0xC75CAFFF, // 0x955586FF, 0xD681C0FF, 0xC985B6FF, 0xC539A6FF, 0xC725A5FF, 0xF44BCDFF, 0x974080FF, 0xD3AFC8FF, // 0xAD5D96FF, 0xDF66BEFF, 0xC82FA3FF, 0xC849A5FF, 0x90567EFF, 0xE15DBAFF, 0xFA51CAFF, 0xD983BBFF, // 0x861A68FF, 0xEC2AB6FF, 0xB982A4FF, 0xA57B95FF, 0x553549FF, 0xF632BDFF, 0xBB3B90FF, 0xE971BCFF, // 0xCB2C98FF, 0xB46093FF, 0xBA2E8BFF, 0xF158B9FF, 0xA97191FF, 0xA77090FF, 0x953170FF, 0x633750FF, // 0x6F3D59FF, 0xC03D8DFF, 0x58103FFF, 0xC8028CFF, 0x411630FF, 0xEB4CACFF, 0x8B697BFF, 0xAA7C94FF, // 0xD959A3FF, 0xBD3888FF, 0xD073A6FF, 0x79616DFF, 0x9D567CFF, 0x612E4AFF, 0xB8578BFF, 0xFDA7D3FF, // 0xCF86ABFF, 0xA75681FF, 0xA43574FF, 0xF9A5CFFF, 0xC69AAFFF, 0xB93B81FF, 0x937482FF, 0x7F335BFF, // 0xF686BEFF, 0xF15FABFF, 0x6F274DFF, 0xCC3A8AFF, 0xA74577FF, 0xD1478FFF, 0xA64F79FF, 0x876272FF, // 0xAD8596FF, 0x86536AFF, 0xDD3A8FFF, 0x6C4C5AFF, 0xEC3E97FF, 0x8C2E5CFF, 0xC17093FF, 0xCD267EFF, // 0xC17D99FF, 0xC095A5FF, 0xA93F71FF, 0xAB577BFF, 0xB7909FFF, 0xA24D72FF, 0xD5A2B5FF, 0xB42A6DFF, // 0xE32887FF, 0xF661A2FF, 0xF01489FF, 0xCE4F86FF, 0x8B4863FF, 0xB03D6FFF, 0xB25579FF, 0xE3A6BBFF, // 0xE6689AFF, 0x9D7683FF, 0xD3417DFF, 0x954060FF, 0xCC879EFF, 0x981953FF, 0xC37F95FF, 0xF899B8FF, // 0x89495FFF, 0xC48699FF, 0x401426FF, 0xD9397AFF, 0xAF476DFF, 0xC34675FF, 0xC94276FF, 0xB3436CFF, 0x00000000, 0x000000FF, 0xFFFFFFFF, 0x080808FF, 0x101010FF, 0x181818FF, 0x202020FF, 0x292929FF, 0x313131FF, 0x393939FF, 0x414141FF, 0x4A4A4AFF, 0x525252FF, 0x5A5A5AFF, 0x626262FF, 0x6A6A6AFF, 0x737373FF, 0x7B7B7BFF, 0x838383FF, 0x8B8B8BFF, 0x949494FF, 0x9C9C9CFF, 0xA4A4A4FF, 0xACACACFF, 0xB4B4B4FF, 0xBDBDBDFF, 0xC5C5C5FF, 0xCDCDCDFF, 0xD5D5D5FF, 0xDEDEDEFF, 0xE6E6E6FF, 0xEEEEEEFF, 0xF6F6F6FF, 0xF225B9FF, 0xE3DB38FF, 0xAD8A65FF, 0x77377BFF, 0x33D8C9FF, 0x2B7CC9FF, 0xF5B43DFF, 0xC85B67FF, 0x379B2EFF, 0xF32FEDFF, 0xACF7A5FF, 0x194847FF, 0x909DCEFF, 0x7041C7FF, 0xCD1D7DFF, 0x78752CFF, 0xF2C2A4FF, 0x4C173DFF, 0xBF68C0FF, 0xCD9327FF, 0x95403AFF, 0xCB20C5FF, 0x76D973FF, 0x53859AFF, 0x3A1E7EFF, 0xDE5C23FF, 0xE7F83EFF, 0xB6A66BFF, 0x2B0B16FF, 0x8D4F87FF, 0x3EF0E9FF, 0xE01A2CFF, 0x398CF1FF, 0x4A1CE2FF, 0xCD717CFF, 0x9B1E90FF, 0x6AB634FF, 0x2B6D54FF, 0xA5B8EEFF, 0x110F3AFF, 0x8A5FCCFF, 0xEF2584FF, 0x8E8C2EFF, 0xECDEA6FF, 0x63344DFF, 0xC885D4FF, 0x991FC6FF, 0xD7AD2AFF, 0xA75744FF, 0xE559C6FF, 0x64F888FF, 0x619CA3FF, 0x655194FF, 0xED7228FF, 0xBB224CFF, 0xC9BD89FF, 0x452515FF, 0xB36286FF, 0xF5332BFF, 0x44A4F3FF, 0x5244EAFF, 0x84451FFF, 0xF1897EFF, 0xB64C8FFF, 0x70CD38FF, 0x568A70FF, 0x8CD6ECFF, 0x1D3665FF, 0x937DEAFF, 0xF3668BFF, 0x97A536FF, 0x8F5A72FF, 0xE98DEEFF, 0xB024EEFF, 0x1F359CFF, 0xB5704CFF, 0x8B1C63FF, 0xEB6EDCFF, 0x256323FF, 0x4FBFB0FF, 0x6864A0FF, 0xD24059FF, 0xA0C4AEFF, 0x1C1EE8FF, 0x26491BFF, 0xA67FAAFF, 0x823DA9FF, 0x37CCF1FF, 0x3771EDFF, 0x965B20FF, 0xF1A385FF, 0x52111AFF, 0xA9A385FF, 0x4AF53BFF, 0x39A67BFF, 0x94F3EAFF, 0x215175FF, 0x828CF3FF, 0x7521E8FF, 0x9A2418FF, 0x9FBC3AFF, 0xA27A83FF, 0xDEAFEBFF, 0x511671FF, 0xA95CECFF, 0x2751B4FF, 0xCF8A58FF, 0x7D5054FF, 0x297B27FF, 0x77D6B5FF, 0x6073BBFF, 0x4B1BA9FF, 0xE87461FF, 0xE5F182FF, 0x525324FF, 0xC396BEFF, 0x8B52A8FF, 0x9F6D24FF, 0x74181FFF, 0xC581A7FF, 0x3BB678FF, 0x27648BFF, 0x7C51EBFF, 0xB13F25FF, 0xDF4C9EFF, 0xA0DD3AFF, 0x7E946DFF, 0xEBC4EAFF, 0x5B3883FF, 0xB383F2FF, 0x286BBBFF, 0xDDA562FF, 0x8D6562FF, 0x378A33FF, 0xD548ECFF, 0x5AF6C2FF, 0x708EC7FF, 0x543CB3FF, 0xF06468FF, 0xA8246BFF, 0x596D2BFF, 0xCBA4BCFF, 0x35134AFF, 0xA762AFFF, 0xF42E5CFF, 0xB38427FF, 0x803B47FF, 0xEF78B9FF, 0xAF45B2FF, 0x3ACD85FF, 0x297894FF, 0x1B1988FF, 0xC05924FF, 0xED8AA7FF, 0xA7F73FFF, 0x82A77BFF, 0x844A6FFF, 0xBE2123FF, 0x3393CBFF, 0xD0BA67FF, 0x1D1AB9FF, 0xAE6D80FF, 0x6E456CFF, 0x31AD2DFF, 0x246161FF, 0x7AAFE2FF, 0x5E5AC9FF, 0xB7466FFF, 0x6A8B32FF, 0xAFDCB8FF, 0x453863FF, 0x9C73D1FF, 0x731EB9FF, 0xB4A62EFF, 0x99685CFF, 0xF2A1C5FF, 0xAA7FC5FF, 0x38E996FF, 0x2693A1FF, 0x31488BFF, 0x111212FF, 0x951D3FFF, 0x91BC7FFF, 0x182E3FFF, 0x87668BFF, 0xD2442BFF, 0x3DA9D0FF, 0xDCD17AFF, 0x243FDEFF, 0x61381BFF, 0xC98A8BFF, 0x857081FF, 0x2EC534FF, 0x2D7F6BFF, 0x74C5DEFF, 0x151764FF, 0x7371EAFF, 0xDB5B84FF, 0x75A33DFF, 0xDDF8C6FF, 0x55567DFF, 0xAF9CEFFF, 0x9645E3FF, 0xC6C634FF, 0x8A684EFF, 0x6C1755FF, 0xC970EBFF, 0x32AEADFF, 0x486597FF, 0xF29032FF, 0xB0565CFF, 0xA3D385FF, 0x1B3216FF, 0x8674ACFF, 0x6F1A8CFF, 0xF06041FF, 0x32B8E5FF, 0x265AE8FF, 0x725A24FF, 0xD1A391FF, 0x948C7FFF, 0x38DE39FF, 0x29936AFF, 0x5C1AD9FF, 0x711E1AFF, 0xF07095FF, 0x85B557FF, 0x5C6365FF, 0xC7B7EBFF, 0x2C054DFF, 0x9C59E5FF, 0xFC2091FF, 0xDAD818FF, 0xA9885BFF, 0x70365EFF, 0xE682E6FF, 0x60C8B0FF, 0x4A71AEFF, 0xFAA737FF, 0xBD565BFF, 0xF93CDFFF, 0xBEEF9DFF, 0x31432FFF, 0x989BADFF, 0x6D41A3FF, 0x3079FDFF, 0x7C681BFF, 0xE9BFA0FF, 0x3F1A28FF, 0xB569A8FF, 0x33FB52FF, 0x25A775FF, 0x165271FF, 0x84A9FFFF, 0x6943F0FF, 0x8A3923FF, 0xC426A0FF, 0x90CF5DFF, 0x6B7D72FF, 0xD5D2F8FF, 0x3F2864FF, 0xAC76F7FF, 0xE9F211FF, 0xBAA163FF, 0x854F6DFF, 0xF89DF4FF, 0xC232EDFF, 0x68E3BBFF, 0x548CBFFF, 0x3D29A9FF, 0xD27066FF, 0x8E0765FF, 0x3F5C3BFF, 0xA6B5BAFF, 0x130B27FF, 0x7D5DB6FF, 0xDE2E65FF, 0x8D8120FF, 0xF9D9A8FF, 0x3C17F7FF, 0x553336FF, 0xC884B6FF, 0x8F1CABFF, 0x2AC27FFF, 0x1D6D82FF, 0x170562FF, 0xA1532BFF, 0xDB49B0FF, 0x9CE962FF, 0x78967EFF, 0x4F4278FF, 0x9013F8FF, 0xA7082EFF, 0xCABB6BFF, 0x98697BFF, 0x6FFEC5FF, 0x5CA7CFFF, 0x4849C0FF, 0xE6896FFF, 0xA63175FF, 0x4C7645FF, 0xB3CFC5FF, 0x24253DFF, 0x8C78C7FF, 0xF74E70FF, 0x9D9B24FF, 0x694C43FF, 0xD99EC3FF, 0xA440BEFF, 0x2FDC88FF, 0x248791FF, 0x1C2C7CFF, 0xB66C31FF, 0x71173DFF, 0xF066BFFF, 0x85B089FF, 0x5F5C89FF, 0xC13337FF, 0xD9D571FF, 0x1028C5FF, 0x3B2C0CFF, 0xA98288FF, 0x72297FFF, 0x64C1DDFF, 0x5166D4FF, 0xFAA377FF, 0xBD4E83FF, 0x578F4FFF, 0xBFEAD0FF, 0x323E4FFF, 0x9B92D7FF, 0x712DC7FF, 0xACB426FF, 0x7B654FFF, 0xE9B8CFFF, 0x401042FF, 0xB75ED0FF, 0x33F790FF, 0x2AA29FFF, 0x224992FF, 0xC98637FF, 0x8A344BFF, 0x90CA93FF, 0x041D1AFF, 0x6D7699FF, 0x431382FF, 0xD9503FFF, 0xE8EF76FF, 0x4E4515FF, 0xBA9C93FF, 0x864691FF, 0x6CDCEAFF, 0x5A81E7FF, 0x561817FF, 0xD26991FF, 0x62A957FF, 0x40575FFF, 0xA8ADE6FF, 0x814EDCFF, 0xDE1D8BFF, 0xBACE27FF, 0x8C7E59FF, 0xF9D3DBFF, 0x562C55FF, 0xC979E0FF, 0x2FBCACFF, 0x2864A6FF, 0xDC9F3CFF, 0xA04E58FF, 0x338607FF, 0xDC35D8FF, 0x9BE59CFF, 0x123629FF, 0x7A90A8FF, 0x533599FF, 0xF06B46FF, 0x605E1DFF, 0xCAB69EFF, 0x26111FFF, 0x9960A1FF, 0x72F7F6FF, 0xFB1A4AFF, 0x639DF9FF, 0x5133E5FF, 0x6E3221FF, 0xE6839DFF, 0xA71F98FF, 0x6CC45EFF, 0x4C716DFF, 0xB5C8F4FF, 0x281A58FF, 0x906BEFFF, 0xF74499FF, 0xC8E926FF, 0x9C9762FF, 0x694666FF, 0xDB94EFFF, 0xA726E4FF, 0x33D7B8FF, 0x2D7FB8FF, 0xEEB93FFF, 0x28149CFF, 0xB56863FF, 0x3CA00CFF, 0xF257E8FF, 0xA5FFA4FF, 0x1E5036FF, 0x86AAB6FF, 0x6251ADFF, 0xC12B60FF, 0x707723FF, 0xD9D0A7FF, 0x3B292FFF, 0xAB7BB1FF, 0x750BA0FF, 0x5A55FBFF, 0x844B2AFF, 0xF99DA8FF, 0xBE42A9FF, 0x76DE64FF, 0x588B7AFF, 0x36366EFF, 0x8A032BFF, 0xABB16BFF, 0x7C5F75FF, 0xEBAFFDFF, 0xBA4CF7FF, 0x36F2C3FF, 0x339AC8FF, 0xFFD341FF, 0x2E3AB4FF, 0xC9816DFF, 0x8A2A6DFF, 0x44BB0CFF, 0x286942FF, 0x92C5C2FF, 0x0B1731FF, 0x706CC0FF, 0xD9496CFF, 0x7F9029FF, 0xE7EAB0FF, 0x4E413EFF, 0xBB95BFFF, 0x8936B5FF, 0x996432FF, 0x561036FF, 0xD35FB9FF, 0x7FF96AFF, 0x63A586FF, 0x435081FF, 0xA42E35FF, 0xB9CB72FF, 0x232109FF, 0x8D7983FF, 0x581F74FF, 0x37B5D8FF, 0x3458CBFF, 0xDB9B76FF, 0xA0477DFF, 0x4BD509FF, 0x32834CFF, 0x9CDFCEFF, 0x163146FF, 0x7D87D1FF, 0x591CBBFF, 0xF06577FF, 0x8CAA2DFF, 0x5F5A4BFF, 0xCBAFCCFF, 0x9B54C8FF, 0xAC7D39FF, 0x6E2D45FF, 0xE77AC8FF, 0x6DBF91FF, 0x4F6A92FF, 0xBC4A3EFF, 0xF832C1FF, 0xC7E578FF, 0x343914FF, 0x9C9290FF, 0x6C3C88FF, 0x3BD0E6FF, 0x3974DFFF, 0xEDB57EFF, 0x3B1012FF, 0xB5618CFF, 0x3B9E56FF, 0xA6FAD8FF, 0x214B57FF, 0x89A2E0FF, 0x6841D2FF, 0xC11985FF, 0x9AC430FF, 0x6F7456FF, 0xDAC9D8FF, 0x3D224CFF, 0xAD70D9FF, 0xBE963FFF, 0x844653FF, 0xFA95D5FF, 0xC02CCFFF, 0x76DA9BFF, 0x5B85A2FF, 0x3C278DFF, 0xD36446FF, 0xD3FF7EFF, 0x44521DFF, 0xABAC9BFF, 0x0F0513FF, 0x7D569AFF, 0x3DEBF3FF, 0xDC1A48FF, 0xFECF86FF, 0x3B1CD8FF, 0x53291EFF, 0xC97B99FF, 0x8C158FFF, 0x42B85EFF, 0x2B6567FF, 0x95BDEFFF, 0x140649FF, 0x755FE7FF, 0xD93F94FF, 0xA6DE31FF, 0x7E8D60FF, 0xE8E4E3FF, 0x4F3B5EFF, 0xBD8BE9FF, 0x8D14D9FF, 0xCFB043FF, 0x98605FFF, 0xD54FE1FF, 0x7EF4A4FF, 0x659FB1FF, 0x4945A3FF, 0xE87E4DFF, 0xA4265BFF, 0x526C25FF, 0xB9C6A5FF, 0x231E27FF, 0x8E71AAFF, 0xF64051FF, 0x4145F0FF, 0x694228FF, 0xDB95A5FF, 0xA23AA2FF, 0x4AD265FF, 0x347F75FF, 0x9FD7FDFF, 0x1D2862FF, 0x827BF9FF, 0xF05CA2FF, 0xB2F931FF, 0x8CA76AFF, 0xF5FEEDFF, 0x60556FFF, 0xCDA6F8FF, 0x9F40EEFF, 0xBF210EFF, 0xDFCA47FF, 0x1428A8FF, 0xAC796AFF, 0x6F2265FF, 0xE96DF2FF, 0x6FB9BEFF, 0x5460B7FF, 0xFC9853FF, 0xBC4468FF, 0x60852BFF, 0xC6E0AFFF, 0x0A0FF5FF, 0x343637FF, 0x9E8BB9FF, 0x6F2AABFF, 0x7D5B31FF, 0xEDAFB0FF, 0x3B062CFF, 0xB757B3FF, 0x50ED6CFF, 0x3C9982FF, 0x274377FF, 0x6F22F8FF, 0x872932FF, 0x99C172FF, 0x0B1405FF, 0x706E7EFF, 0x401068FF, 0xD84213FF, 0xEEE44AFF, 0x0B49C0FF, 0xBD9274FF, 0x853E76FF, 0x78D4CAFF, 0x5F7BC9FF, 0xD25F74FF, 0x6C9F31FF, 0xD3FBB8FF, 0x444F45FF, 0xADA5C7FF, 0x8149BFFF, 0x8F7439FF, 0xFEC9BAFF, 0x53243EFF, 0xCA72C2FF, 0x44B48EFF, 0x305E8AFF, 0x9F433CFF, 0xDA2CBAFF, 0xA5DB79FF, 0x192D11FF, 0x7F888BFF, 0x52307EFF, 0xF05E16FF, 0xFDFE4BFF, 0xCEAC7EFF, 0x21070CFF, 0x995886FF, 0x80EFD6FF, 0x6A96DAFF, 0x5032C7FF, 0xE7797FFF, 0xA4127DFF, 0x78B935FF, 0x526852FF, 0xBABFD4FF, 0x251640FF, 0x9165D1FF, 0xF5367DFF, 0xA08D40FF, 0x693D4DFF, 0xDC8DD0FF, 0xA422C6FF, 0x4ACE99FF, 0x39789BFF, 0x271580FF, 0xB55D45FF, 0xF14FCBFF, 0xB0F580FF, 0x27461BFF, 0x8CA298FF, 0x634C92FF, 0xBE1845FF, 0xDEC686FF, 0x392019FF, 0xAC7294FF, 0x73B1EAFF, 0x5B52DDFF, 0xFB9388FF, 0xBC398EFF, 0x83D438FF, 0x5F825EFF, 0xC8DAE0FF, 0x363055FF, 0xA181E3FF, 0xB0A746FF, 0x7D565BFF, 0xEEA7DEFF, 0xB946D9FF, 0x50E9A2FF, 0x4093ABFF, 0x2F3798FF, 0xCA774DFF, 0x872054FF, 0x336024FF, 0x99BCA3FF, 0x0C111CFF, 0x7266A3FF, 0xD83C4FFF, 0xEEE08DFF, 0x2832E4FF, 0x4E3825FF, 0xBE8CA1FF, 0x873199FF, 0x7CCCF8FF, 0x666FF1FF, 0xD2569CFF, 0x8DEE3AFF, 0x6C9C68FF, 0xD4F4EBFF, 0x454967FF, 0xAF9CF2FF, 0x8533E4FF, 0xA11E12FF, 0xC0C04BFF, 0x8F7067FF, 0xFEC2EAFF, 0x55195BFF, 0xCC65EBFF, 0x48ADB9FF, 0x3853AEFF, 0xDE9054FF, 0x9F3D63FF, 0x3E7A2CFF, 0xA5D6ADFF, 0x1A2A2FFF, 0x8180B3FF, 0x571B9FFF, 0xEF5958FF, 0xFCFA93FF, 0x2655FBFF, 0x61512FFF, 0xCFA6ADFF, 0x9B4EABFF, 0x6B222DFF, 0xE871AAFF, 0x77B671FF, 0x536377FF, 0x9655F9FF, 0xBB3D18FF, 0xF524A5FF, 0xCEDA4FFF, 0xA08972FF, 0x6A356EFF, 0xDF81FBFF, 0x4EC8C6FF, 0x406FC1FF, 0xF0AA5AFF, 0xB55870FF, 0x499433FF, 0xF33AF3FF, 0xB0F0B7FF, 0x27433FFF, 0x8E9BC2FF, 0x673DB5FF, 0x736A38FF, 0xDFC0B8FF, 0x3A1B35FF, 0xAE69BCFF, 0x833C39FF, 0xFC8CB6FF, 0xBE25B3FF, 0x82D079FF, 0x617D86FF, 0x3A2473FF, 0xD3581DFF, 0xDCF552FF, 0xB0A37CFF, 0x7E4F7FFF, 0x54E3D3FF, 0x478AD3FF, 0x3A1FBAFF, 0xCA727BFF, 0x880575FF, 0x53AE39FF, 0x335C4DFF, 0x9BB5D0FF, 0x110431FF, 0x765AC9FF, 0xD73378FF, 0x838340FF, 0xEEDAC2FF, 0x4F3446FF, 0xBF84CBFF, 0x8A12BCFF, 0x995543FF, 0xD448C4FF, 0x8CEA81FF, 0x6D9793FF, 0x494088FF, 0xE97221FF, 0xA11541FF, 0xBFBC85FF, 0x211613FF, 0x90698EFF, 0x59FEDEFF, 0xF42E2FFF, 0x4EA5E3FF, 0x4243D2FF, 0xDD8B86FF, 0xA03286FF, 0x5CC83DFF, 0x3F765AFF, 0xA7CFDDFF, 0x1E234AFF, 0x8476DBFF, 0xEF5185FF, 0x929D48FF, 0xFCF5CBFF, 0x614C55FF, 0xD09ED9FF, 0x9E3CD1FF, 0x16268BFF, 0xAD6F4CFF, 0x6B194CFF, 0xE966D4FF, 0x78B1A0FF, 0x575A9BFF, 0xFE8C23FF, 0xBA384CFF, 0xCDD68DFF, 0x0E18D6FF, 0x352D20FF, 0xA1839CFF, 0x6C268FFF, 0x54C0F3FF, 0x4961E7FF, 0xF0A590FF, 0xB64F96FF, 0x64E341FF, 0x499065FF, 0xB2EAE8FF, 0x2A3D5EFF, 0x9291ECFF, 0x6D23D9FF, 0x841913FF, 0xA0B64EFF, 0x736662FF, 0xE0B9E6FF, 0x3C0C4FFF, 0xB05BE3FF, 0x1745A3FF, 0xC08854FF, 0x83365CFF, 0xFD82E2FF, 0x0E6D2BFF, 0x82CBABFF, 0x6475ACFF, 0xD25357FF, 0xDBF194FF, 0x46462CFF, 0xB19DA9FF, 0x8044A3FF, 0x517EFBFF, 0x501B27FF, 0xCA6AA5FF, 0x6BFD43FF, 0x52AA6FFF, 0x365770FF, 0x9EACFCFF, 0x7C48EFFF, 0x9E371BFF, 0xD81F9EFF, 0xADD053FF, 0x837F6EFF, 0xEFD3F3FF, 0x502B64FF, 0xC278F5FF, 0x1861B8FF, 0xD2A25BFF, 0x99506BFF, 0x188733FF, 0xD631EBFF, 0x8CE6B5FF, 0x6F8FBCFF, 0x4F30AAFF, 0xE86E60FF, 0x565F36FF, 0xC0B7B5FF, 0x22102AFF, 0x925FB4FF, 0xF32460FF, 0xA48203FF, 0x4D26F7FF, 0x683434FF, 0xDE84B2FF, 0xA21BAAFF, 0x5BC578FF, 0x417180FF, 0x241365FF, 0xB55221FF, 0xF045ADFF, 0xBAEB57FF, 0x929979FF, 0xFDEEFEFF, 0x634577FF, 0xA21AF7FF, 0x197DCBFF, 0xE3BB62FF, 0xAD6977FF, 0x20A23AFF, 0xEB55FDFF, 0x7BAACBFF, 0x5C4EBFFF, 0xFD8868FF, 0xBA2E72FF, 0x657840FF, 0xCED1C0FF, 0x35293DFF, 0xA37AC5FF, 0xB59C00FF, 0x7D4D40FF, 0xF09EBFFF, 0xB740BCFF, 0x63DF81FF, 0x4B8B8EFF, 0x30337DFF, 0xCB6B26FF, 0x84103CFF, 0xA0B283FF, 0x0A0A0AFF, 0x745F88FF, 0xD62C30FF, 0x1998DDFF, 0xF3D567FF, 0x2932C6FF, 0xC08383FF, 0x842A7EFF, 0x27BC40FF, 0x116A55FF, 0x85C4D8FF, 0x04143CFF, 0x686AD3FF, 0xD24C80FF, 0x739248FF, 0xDBEBCAFF, 0x47424EFF, 0xB294D4FF, 0x8331C7FF, 0x90664AFF, 0x511043FF, 0xCC5ECDFF, 0x54A59BFF, 0x3B4E92FF, 0xE0852AFF, 0x9D3248FF, 0xADCC8CFF, 0x1C221AFF, 0x847997FF, 0x541984FF, 0xEF4B36FF, 0x18B4EDFF, 0x2B53DDFF, 0x624707FF, 0xD29D8EFF, 0x9A468FFF, 0x2CD745FF, 0x1A8461FF, 0x8EDFE5FF, 0x092F53FF, 0x7485E5FF, 0x681412FF, 0xE8678CFF, 0x80AC4FFF, 0x575B5DFF, 0xC1AFE2FF, 0x9551DBFF, 0xF30888FF, 0xA37F53FF, 0x682D55FF, 0xDF7ADDFF, 0x5DC0A8FF, 0x4669A5FF, 0xF49F2DFF, 0xB54D54FF, 0xF131D5FF, 0xB9E795FF, 0x2B3A27FF, 0x9392A4FF, 0x663999FF, 0x16CFFCFF, 0x2D70F2FF, 0x755F0FFF, 0xE3B798FF, 0x361220FF, 0xAE619FFF, 0x31F249FF, 0x219E6DFF, 0x97FAF1FF, 0x114A67FF, 0x7FA0F6FF, 0x633AE4FF, 0x81311BFF, 0xFD8298FF, 0xBB1997FF, 0x8CC655FF, 0x65746AFF, 0xCFC9EFFF, 0x381F59FF, 0xA66DEDFF, 0xB4995BFF, 0x7D4764FF, 0xF295EBFF, 0xBA26E3FF, 0x64DAB3FF, 0x4F83B6FF, 0x381F9DFF, 0xCA675EFF, 0x3A5333FF, 0xA1ADB1FF, 0x7755ACFF, 0xD5225DFF, 0x877914FF, 0xF3D1A0FF, 0x4D2B2EFF, 0xC17CADFF, 0x860CA0FF, 0x27B977FF, 0x6DFA89FF, 0x196479FF, 0x3E91F1FF, 0x6F5AFAFF, }; /** * 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. *
* Internally, this is similar to {@code (float)Math.pow(L, 1.5f)}. At one point it used a modified "Barron spline" * to get its curvature mostly right, but this now seems nearly indistinguishable from an ideal curve. * @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) { return (float) Math.sqrt(L * L * L); } /** * Changes the curve of the internally-used lightness when it is output to another format. This makes the very-dark * area smaller, matching (closely) the curve that the standard sRGB lightness uses. This is typically used on "from * Oklab" conversions. *
* Internally, this is similar to {@code (float)Math.pow(L, 2f/3f)}. At one point it used a modified "Barron spline" * to get its curvature mostly right, but this now seems nearly indistinguishable from an ideal curve. *
* This specific code uses a modified cube root approximation (based on {@link OtherMath#cbrtPositive(float)}) * originally by Marc B. Reynolds. * @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) { int ix = NumberUtils.floatToRawIntBits(L); final float x0 = L; ix = (ix>>>2) + (ix>>>4); ix += (ix>>>4); ix += (ix>>>8) + 0x2A5137A0; L = NumberUtils.intBitsToFloat(ix); L = 0.33333334f * (2f * L + x0/(L*L)); L = 0.33333334f * (1.9999999f * L + x0/(L*L)); return L * L; } // /** // * 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-red axis, from -0.5 (green) to 0.5 (red). * OKLAB[2] stores B, which is something like a blue-yellow 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 {@code 4.232604}, and the other half should * be the inverses of the first half (between {@code 0.23626116}, which is {@code 1.0/4.232604}, and 1). *
* While, for some reason, you could change the contents to some other distribution of floats, I don't know why this * would be needed. */ public static final float[] TRI_BLUE_NOISE_MULTIPLIERS = ConstantData.TRI_BLUE_NOISE_MULTIPLIERS; /** * 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_B}. 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 {@code 4.232604}, and the other half should * be the inverses of the first half (between {@code 0.23626116}, which is {@code 1.0/4.232604}, and 1). *
* While, for some reason, you could change the contents to some other distribution of floats, I don't know why this * would be needed. */ public static final float[] TRI_BLUE_NOISE_MULTIPLIERS_B = ConstantData.TRI_BLUE_NOISE_MULTIPLIERS_B; /** * 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_C}. 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 {@code 4.232604}, and the other half should * be the inverses of the first half (between {@code 0.23626116}, which is {@code 1.0/4.232604}, and 1). *
* While, for some reason, you could change the contents to some other distribution of floats, I don't know why this * would be needed. */ public static final float[] TRI_BLUE_NOISE_MULTIPLIERS_C = ConstantData.TRI_BLUE_NOISE_MULTIPLIERS_C; 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.cbrtPositive(0.4121656120f * rf + 0.5362752080f * gf + 0.0514575653f * bf); mf = OtherMath.cbrtPositive(0.2118591070f * rf + 0.6807189584f * gf + 0.1074065790f * bf); sf = OtherMath.cbrtPositive(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); } /** * Given a non-null Pixmap, this finds the up-to-255 most-frequently-used colors and returns them as an array of * RGBA8888 ints. This always reserves space in the returned array for fully-transparent, so there can be in full * 256 colors in the returned array. The int array this returns is useful to pass to {@link #exact(int[])} or the * constructors that expect an RGBA8888 palette. * @param pixmap a non-null Pixmap, often representing or already limited to the desired palette * @return an array of between 1 and 256 RGBA8888 ints, representing a palette */ public static int[] colorsFrom(Pixmap pixmap) { return colorsFrom(pixmap, 256); } /** * Given a non-null Pixmap, this finds the up-to-{@code limit - 1} most-frequently-used colors and returns them as * an array of RGBA8888 ints. This always reserves space in the returned array for fully-transparent, so there can * be in full {@code limit} colors in the returned array. The int array this returns is useful to pass to * {@link #exact(int[])} or the constructors that expect an RGBA8888 palette. * @param pixmap a non-null Pixmap, often representing or already limited to the desired palette * @param limit how many colors this can return as an inclusive limit; the actual returned array can be smaller * @return an array of between 1 and {@code limit} RGBA8888 ints, representing a palette */ public static int[] colorsFrom(Pixmap pixmap, int limit) { int color, colorCount; final int width = pixmap.getWidth(), height = pixmap.getHeight(); IntIntMap counts = new IntIntMap(256); 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); colorCount = Math.min(limit, es.size + 1); int[] colorArray = new int[colorCount]; int i = 1; for (IntIntMap.Entry e : es) { color = e.key; colorArray[i] = color; if(++i >= limit) break; } return colorArray; } /** * 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]; /** * A FloatArray used as a buffer to store accrued error for error-diffusion dithers. * This stores error for the current line's red channel. * It is protected so that user code that extends PaletteReducer doesn't need to create its own buffers. */ protected transient FloatArray curErrorRedFloats; /** * A FloatArray used as a buffer to store accrued error for error-diffusion dithers. * This stores error for the next line's red channel. * It is protected so that user code that extends PaletteReducer doesn't need to create its own buffers. */ protected transient FloatArray nextErrorRedFloats; /** * A FloatArray used as a buffer to store accrued error for error-diffusion dithers. * This stores error for the current line's green channel. * It is protected so that user code that extends PaletteReducer doesn't need to create its own buffers. */ protected transient FloatArray curErrorGreenFloats; /** * A FloatArray used as a buffer to store accrued error for error-diffusion dithers. * This stores error for the next line's green channel. * It is protected so that user code that extends PaletteReducer doesn't need to create its own buffers. */ protected transient FloatArray nextErrorGreenFloats; /** * A FloatArray used as a buffer to store accrued error for error-diffusion dithers. * This stores error for the current line's blue channel. * It is protected so that user code that extends PaletteReducer doesn't need to create its own buffers. */ protected transient FloatArray curErrorBlueFloats; /** * A FloatArray used as a buffer to store accrued error for error-diffusion dithers. * This stores error for the next line's blue channel. * It is protected so that user code that extends PaletteReducer doesn't need to create its own buffers. */ protected transient FloatArray nextErrorBlueFloats; /** * How many colors are in the palette here; this is at most 256, and typically includes one fully-transparent color. */ public int colorCount; /** * Determines how strongly to apply noise or other effects during dithering. The neutral value is 1.0f . */ 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; /** * Given by Joel Yliluoma in a dithering article. * Must not be modified. */ protected static final int[] thresholdMatrix8 = { 0, 4, 2, 6, 3, 7, 1, 5, }; /** * Given by Joel Yliluoma in a dithering article. * Must not be modified. */ protected 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. * Must not be modified. */ protected 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 }; /** * A temporary 32-element array typically used to store colors or palette indices along with their RGB555 * {@link #shrink(int) shrunken} analogues, so that {@link #sort16(int[])} can sort them. Mostly for internal use. */ protected transient final int[] candidates = new int[32]; /** * 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 "Snuggly" 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.SNUGGLY)}; this metric would be too slow to calculate at * runtime, but as pre-calculated data it works very well. */ public PaletteReducer() { exact(SNUGGLY, ENCODED_SNUGGLY); } /** * 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(SNUGGLY, ENCODED_SNUGGLY); 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(SNUGGLY, ENCODED_SNUGGLY); 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(SNUGGLY, ENCODED_SNUGGLY); 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(SNUGGLY, ENCODED_SNUGGLY); 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(SNUGGLY, ENCODED_SNUGGLY); 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(SNUGGLY, ENCODED_SNUGGLY); 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 300) */ 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) { final int idx1 = ((r1 << 7) & 0x7C00) | ((g1 << 2) & 0x3E0) | ((b1 >>> 3)); final int idx2 = ((r2 << 7) & 0x7C00) | ((g2 << 2) & 0x3E0) | ((b2 >>> 3)); final double dL = (OKLAB[0][idx1] - OKLAB[0][idx2]) * 512.0; final double dA = (OKLAB[1][idx1] - OKLAB[1][idx2]) * 512.0; final double dB = (OKLAB[2][idx1] - OKLAB[2][idx2]) * 512.0; return (dL * dL + dA * dA + dB * dB); // 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) { final int idx1 = ((r1 << 7) & 0x7C00) | ((g1 << 2) & 0x3E0) | ((b1 >>> 3)); final int idx2 = ((r2 << 7) & 0x7C00) | ((g2 << 2) & 0x3E0) | ((b2 >>> 3)); final double dL = (OKLAB[0][idx1] - OKLAB[0][idx2]) * 512.0; final double dA = (OKLAB[1][idx1] - OKLAB[1][idx2]) * 512.0; final double dB = (OKLAB[2][idx1] - OKLAB[2][idx2]) * 512.0; return (dL * dL + dA * dA + dB * dB); // 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) { final int idx1 = ((r1 << 7) & 0x7C00) | ((g1 << 2) & 0x3E0) | ((b1 >>> 3)); final int idx2 = ((r2 << 7) & 0x7C00) | ((g2 << 2) & 0x3E0) | ((b2 >>> 3)); final double dL = (OKLAB[0][idx1] - OKLAB[0][idx2]) * 512.0; final double dA = (OKLAB[1][idx1] - OKLAB[1][idx2]) * 512.0; final double dB = (OKLAB[2][idx1] - OKLAB[2][idx2]) * 512.0; return (dL * dL + dA * dA + dB * dB); // 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) "Snuggly" 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.SNUGGLY)}; this metric would be too slow to calculate at * runtime, but as pre-calculated data it works very well. */ public void setDefaultPalette(){ exact(SNUGGLY, ENCODED_SNUGGLY); } /** * 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 "Snuggly" 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 "Snuggly" 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(SNUGGLY, ENCODED_SNUGGLY); 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.375/colorCount); int color, c2; double dist; 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; } } 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(SNUGGLY, 0, paletteArray, 0, 256); System.arraycopy(ENCODED_SNUGGLY, 0, paletteMapping, 0, 0x8000); colorCount = 256; populationBias = (float) Math.exp(-1.125 / 256.0); return; } long startTime = System.currentTimeMillis(); colorCount = Math.min(256, palette.length); System.arraycopy(palette, 0, paletteArray, 0, colorCount); System.arraycopy(preload, 0, paletteMapping, 0, 0x8000); populationBias = (float) Math.exp(-1.375/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 "Snuggly" 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 "Snuggly" 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(SNUGGLY, ENCODED_SNUGGLY); 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.375/colorCount); int color, c2; double dist; 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; } 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; } } } } } } 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 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 * 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 * 300, 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); } /** * 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 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 200, 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 200, 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 200, 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 200, 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.375/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.375/colorCount); } 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 200, 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 200, 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.000043; 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.375/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.375/colorCount); } 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 fewer 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 or AnimatedGif'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. 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 200, 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. If the threshold is too low, many similar colors may be chosen * at the expense of some less common, but still important, 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. *
* 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 200, 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.375/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.375/colorCount); } 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.375/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.375/colorCount); } 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; } } } } } } protected static boolean bigPaletteLoaded = false; protected static char[] bigPaletteMapping; /** * Builds a mapping from RGB555 colors to their closest match in {@code palette} by calculating the closest match * for every color. The {@code palette} should have 1024 colors, if possible, but can be smaller.The mapping this * makes is only used by the "Reductive" analysis methods, such as {@link #analyzeReductive(Pixmap, double, int)}. * Note that while this method is not static, the palette mapping it * stores its result in is static, so you should avoid calling this on multiple threads. * * @param palette a typically-1024-color RGBA8888 palette; may be smaller, but not larger */ public void alterBigPalette(int[] palette) { if(bigPaletteMapping == null) bigPaletteMapping = new char[0x8000]; final int plen = palette.length; // Check reference equality to avoid medium-large copy when building from existing BIG_PALETTE if(palette != BIG_PALETTE) System.arraycopy(palette, 0, BIG_PALETTE, 0, Math.min(plen, BIG_PALETTE.length)); if(plen < BIG_PALETTE.length) Arrays.fill(BIG_PALETTE, plen, 1024, 0); int color, c2; double dist; for (int i = 0; i < plen; i++) { color = palette[i]; if ((color & 0x80) != 0) { bigPaletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (char) 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 (bigPaletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = 1E100; for (int i = 1; i < plen; i++) { if (dist > (dist = Math.min(dist, differenceMatch(BIG_PALETTE[i], rr, gg, bb)))) bigPaletteMapping[c2] = (char) i; } } } } } bigPaletteLoaded = true; } /** * Writes the current {@link #bigPaletteMapping} to the given FileHandle. If {@code filename} is null, this writes * to the local FileHandle {@code "BigPaletteMapping.dat"} . * The palette file can be used by anim8-gdx itself if * its name is {@code "BigPaletteMapping.dat"} and stored in the classpath root, or it can be loaded with * {@link #loadBigPalette(FileHandle, int[])}. * @param filename may be null to write to {@code "BigPaletteMapping.dat"}, or otherwise a FileHandle to write to */ public void writeBigPalette(FileHandle filename){ if(Gdx.files != null && bigPaletteMapping != null) (filename == null ? Gdx.files.local("BigPaletteMapping.dat") : filename).writeString(new String(bigPaletteMapping), false, "UTF8"); } /** * Builds the mapping from RGB555 colors to their closest match in {@code palette} by loading the known mapping * from the given file. This changes {@link #BIG_PALETTE} to use {@code palette}; as such, {@code palette} should * have at most 1024 colors. The mapping this makes is only used by the "Reductive" analysis methods, such as * {@link #analyzeReductive(Pixmap, double, int)}. You typically obtain a palette data file when you call * {@link #writeBigPalette(FileHandle)}, and it can be passed here along with the palette array passed to * {@link #alterBigPalette(int[])} (which does the setup necessary for writeBigPalette()). * Note that while this method is not static, the palette mapping it stores its result in is static, so you should * avoid calling this on multiple threads. * * @param file the FileHandle to load; typically output by {@link #alterBigPalette(int[])} previously * @param palette an array of RGBA8888 int colors; should have length 1024 at most */ public void loadBigPalette(FileHandle file, int[] palette) { final int plen = palette.length; System.arraycopy(palette, 0, BIG_PALETTE, 0, Math.min(plen, BIG_PALETTE.length)); if(plen < BIG_PALETTE.length) Arrays.fill(BIG_PALETTE, plen, 1024, 0); if(bigPaletteMapping == null) bigPaletteMapping = new char[0x8000]; file.readString("UTF8").getChars(0, 0x8000, bigPaletteMapping, 0); bigPaletteLoaded = true; } /** * Builds the mapping from RGB555 colors to their closest match in {@link #BIG_PALETTE} by loading the known mapping * from an optional file, or assembling a new mapping if that file is not present. The file this needs to run using * its "fast path" is {@code BigPaletteMapping.dat}, which must be in the resources root to be loaded successfully * (in a libGDX project, this is {@code /assets/}). You can download this file from * the optional folder of the anim8-gdx repo * or create it yourself by calling this method at least once to * assemble a new mapping, then calling {@link #writeBigPalette(FileHandle)} to create a file that you can then put * in your resources root with the filename as stated before. *
* This will not work as intended if {@link #BIG_PALETTE} has been altered, such as by using * {@link #alterBigPalette(int[])}. The mapping this makes is only used by the "Reductive" analysis methods, such as * {@link #analyzeReductive(Pixmap, double, int)}. If the big palette has already been loaded successfully, this * does nothing and returns immediately (it checks the static field {@link #bigPaletteLoaded}). * Note that while this method is not static, the palette mapping it stores its result in is static, so you should * avoid calling methods that modify {@link #bigPaletteMapping} on other threads (such as * {@link #alterBigPalette(int[])} and {@link #loadBigPalette(FileHandle, int[])}). */ public void buildBigPalette() { if(bigPaletteLoaded) return; if(bigPaletteMapping == null) bigPaletteMapping = new char[0x8000]; FileHandle dat = Gdx.files.classpath("BigPaletteMapping.dat"); if(!dat.exists()) { alterBigPalette(BIG_PALETTE); return; } dat.readString("UTF8").getChars(0, 0x8000, bigPaletteMapping, 0); bigPaletteLoaded = true; } /** * 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 or GIF palette. * This always uses colors from the 1024-color {@link #BIG_PALETTE} palette (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 400, 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. *
* This has a small delay when first called, because it needs to build a mapping for the large {@link #BIG_PALETTE} * palette. This only needs to be done once per program (the result is saved). You can also (in your project) copy * a precalculated mapping into your resources root, as described in {@link #buildBigPalette()}. * * @param pixmap a Pixmap to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)} or by PNG8 */ public void analyzeReductive(Pixmap pixmap) { analyzeReductive(pixmap, 100); } /** * 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 or GIF palette. * This always uses colors from the 1024-color {@link #BIG_PALETTE} palette (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 200, 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 has a small delay when first called, because it needs to build a mapping for the large {@link #BIG_PALETTE} * palette. This only needs to be done once per program (the result is saved). You can also (in your project) copy * a precalculated mapping into your resources root, as described in {@link #buildBigPalette()}. * * @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 200, 100 is a good default */ public void analyzeReductive(Pixmap pixmap, double threshold) { analyzeReductive(pixmap, threshold, 256); } /** * Analyzes {@code pixmap} for color count and frequency, building a palette with at most {@code limit} colors. * This always uses colors from the 1024-color {@link #BIG_PALETTE} palette (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 200, 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 has a small delay when first called, because it needs to build a mapping for the large {@link #BIG_PALETTE} * palette. This only needs to be done once per program (the result is saved). You can also (in your project) copy * a precalculated mapping into your resources root, as described in {@link #buildBigPalette()}. * * @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 200, 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 analyzeReductive(Pixmap pixmap, double threshold, int limit) { buildBigPalette(); Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; limit = Math.min(Math.max(limit, 2), 256); threshold /= Math.min(0.3, Math.pow(limit + 16, 1.45) * 0.00013333); 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 = BIG_PALETTE[bigPaletteMapping[shrink(color)]]; 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.375/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.375/colorCount); } 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; } } } } } } 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 fewer 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 300, 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 fewer 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 200, 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 200, 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 200, 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 200, 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 200, 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 200, 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.375/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.375/colorCount); } 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 fewer 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 500, 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 fewer 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 200, 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 200, 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 200, 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 200, 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 200, 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 200, 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.000043; 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.375/colorCount); 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; } } } } } } /** * 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. This always uses colors from the 1024-color * {@link #BIG_PALETTE} palette (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 400, 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. *
* This has a small delay when first called, because it needs to build a mapping for the large {@link #BIG_PALETTE} * palette. This only needs to be done once per program (the result is saved). Future versions of this library may * use a precalculated mapping. * * @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 analyzeReductive(Array pixmaps){ analyzeReductive(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. This always uses colors from the 1024-color * {@link #BIG_PALETTE} palette (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 200, 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. *
* This has a small delay when first called, because it needs to build a mapping for the large {@link #BIG_PALETTE} * palette. This only needs to be done once per program (the result is saved). Future versions of this library may * use a precalculated mapping. * * @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 200, 100 is a good default */ public void analyzeReductive(Array pixmaps, double threshold){ analyzeReductive(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. This always uses colors from the 1024-color * {@link #BIG_PALETTE} palette (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 200, 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. *
* This has a small delay when first called, because it needs to build a mapping for the large {@link #BIG_PALETTE} * palette. This only needs to be done once per program (the result is saved). Future versions of this library may * use a precalculated mapping. * * @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 200, 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 analyzeReductive(Array pixmaps, double threshold, int limit){ analyzeReductive(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. This always uses colors from the 1024-color * {@link #BIG_PALETTE} palette (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 200, 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. *
* This has a small delay when first called, because it needs to build a mapping for the large {@link #BIG_PALETTE} * palette. This only needs to be done once per program (the result is saved). Future versions of this library may * use a precalculated mapping. * * @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 200, 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 analyzeReductive(Pixmap[] pixmaps, int pixmapCount, double threshold, int limit) { buildBigPalette(); Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; limit = Math.min(Math.max(limit, 2), 256); threshold /= Math.min(0.3, Math.pow(limit + 16, 1.45) * 0.00013333); 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 = BIG_PALETTE[bigPaletteMapping[shrink(color)]]; 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.375/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.375/colorCount); } 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; } } } } } } /** * 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); } public float getPopulationBias() { return populationBias; } /** * Sets the population bias; rarely needed externally. * Typically, the population bias is between 0.5 and 1, closer to 1 with larger palette sizes, and closer to 0.5 * with smaller palettes. *
* Within anim8-gdx, this is generally calculated with {@code (float)Math.exp(-1.375 / colorCount)}, where * {@link #colorCount} is already known and between 2 and 256, inclusive. * * @param populationBias a population bias value, which is almost always between 0.5 and 1.0 */ public void setPopulationBias(float populationBias) { this.populationBias = populationBias; } /** * Modifies the given Pixmap so that it only uses colors present in this PaletteReducer, dithering when it can by * using Overboard dithering (this merely delegates to {@link #reduceOverboard(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 reduceOverboard(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#OVERBOARD} * @return {@code pixmap} after modifications */ public Pixmap reduce(Pixmap pixmap, Dithered.DitherAlgorithm ditherAlgorithm){ if(pixmap == null) return null; if(ditherAlgorithm == null) return reduceOverboard(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); case DODGY: return reduceDodgy(pixmap); case LOAF: return reduceLoaf(pixmap); case NEUE: return reduceNeue(pixmap); case BURKES: return reduceBurkes(pixmap); case WREN: return reduceWren(pixmap); case OCEANIC: return reduceOceanic(pixmap); case SEASIDE: return reduceSeaside(pixmap); case GOURD: return reduceGourd(pixmap); case OVERBOARD: default: return reduceOverboard(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 (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 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; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; float rdiff, gdiff, bdiff; float w1 = ditherStrength * 4, w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f; for (int y = 0; y < h; y++) { int ny = y + 1; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, y, 0); else { int rr = Math.min(Math.max((int)(((color >>> 24) ) + curErrorRed[px] + 0.5f), 0), 0xFF); int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + curErrorGreen[px] + 0.5f), 0), 0xFF); int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + curErrorBlue[px] + 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 (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, y, 0); else { // fract(fract(v_texCoords.xy * vec2(6.711056, 0.583715)) * 52.9829189) 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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 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; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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 = 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 = (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 = (32 * 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 (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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); // Used in 0.3.13, has a heavy color bias // 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); int rr = ((color >>> 24) ); int gg = ((color >>> 16) & 0xFF); int bb = ((color >>> 8) & 0xFF); // We get a sub-random angle from 0-PI2 using the R2 sequence. // This gets us an angle theta from anywhere on the circle, which we feed into three // different cos() calls, each with a different offset to get 3 different angles. final float theta = ((px * 0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL >>> 41) * 0x1.921fb6p-21f); //0x1.921fb6p-21f is 0x1p-23f * MathUtils.PI2 rr = Math.min(Math.max((int)(rr + MathUtils.cos(theta ) * str + 0.5f), 0), 255); gg = Math.min(Math.max((int)(gg + MathUtils.cos(theta + 1.04f) * str + 0.5f), 0), 255); bb = Math.min(Math.max((int)(bb + MathUtils.cos(theta + 2.09f) * 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; } /** * An intentionally low-fidelity dither, meant for pixel art. * @param pixmap * @return */ public Pixmap reduceLoaf(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; final float strength = ditherStrength * populationBias; // this works much better with * instead of / for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, y, 0); else { int adj = (int)((((px + y & 1) << 5) - 16) * strength); // either + 16 * strength or - 16 * strength int rr = Math.min(Math.max(((color >>> 24) ) + adj, 0), 255); int gg = Math.min(Math.max(((color >>> 16) & 0xFF) + adj, 0), 255); int bb = Math.min(Math.max(((color >>> 8) & 0xFF) + adj, 0), 255); int rgb555 = ((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3)); pixmap.drawPixel(px, y, paletteArray[paletteMapping[rgb555] & 0xFF]); } } } pixmap.setBlending(blending); return pixmap; } /** * Primarily used to avoid allocating arrays that copy {@link #thresholdMatrix64}, this has length 64. */ public static final float[] tempThresholdMatrix = new float[64]; /** * A specialized lookup table that takes an index from 0-255 and outputs a float in the 127.5 to 894.5 range. * The output is expected to have a value between -128 and 128 added to it and that given to {@link #fromLinearLUT}. */ public static final float[] toLinearLUT = new float[256]; /** * A specialized lookup table that takes an index from 0-1023 and outputs a byte that should be masked with 255 (to * get a value from 0-255). The input is usually from {@link #toLinearLUT}, with a value in the -128-128 range * added, and this used to get back into the 0-255 range (with the mask). */ public static final byte[] fromLinearLUT = new byte[1024]; static { for (int i = 0; i < 256; i++) { toLinearLUT[i] = (float) Math.pow(i / 255.0, 1.0/2.2) * 767 + 127.5f; } for (int i = 0; i < 1024; i++) { fromLinearLUT[i] = (byte) (Math.pow(Math.min(Math.max(i - 127.5, 0), 767) / 767.0, 2.2) * 255); } } /** * A higher-quality relative of {@link #reduceLoaf(Pixmap)} that uses a 8x8 grid instead of a 2x2 checkerboard, and * that gamma-corrects its changes. * @param pixmap * @return */ public Pixmap reduceGourd(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; final float strength = (float)(ditherStrength * 1.25 * Math.pow(populationBias, -6.0)); for (int i = 0; i < 64; i++) { tempThresholdMatrix[i] = Math.min(Math.max((PaletteReducer.thresholdMatrix64[i] - 31.5f) * strength, -127), 127); } for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, y, 0); else { float adj = tempThresholdMatrix[(px & 7) | (y & 7) << 3]; int rr = fromLinearLUT[(int)(toLinearLUT[(color >>> 24) ] + adj)] & 255; int gg = fromLinearLUT[(int)(toLinearLUT[(color >>> 16) & 0xFF] + adj)] & 255; int bb = fromLinearLUT[(int)(toLinearLUT[(color >>> 8) & 0xFF] + adj)] & 255; int rgb555 = ((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3)); pixmap.drawPixel(px, y, paletteArray[paletteMapping[rgb555] & 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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 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; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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; } /** * Just as the wren flits restlessly from eave to branch to awning, so too does Wren dither dart to and fro between * dithering techniques. This is an error diffusion dither modeled closely after {@link #reduceWoven(Pixmap)}, which * means it uses three offset versions of the R2 sequence to introduce a structured artifact that breaks up * Floyd-Steinberg artifacts. It also incorporates per-channel blue-noise effects as {@link #reduceDodgy(Pixmap)} * uses them. The strengths of various components here have changed from the values used in Woven; the * error-diffusion strength is higher by default and the adjustment from blue noise and/or R2 values is * comparatively mild. * @param pixmap * @return */ public Pixmap reduceWrenOriginal(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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used; float rdiff, gdiff, bdiff; float er, eg, eb; byte paletteIndex; final float w1 = (float) (32.0 * ditherStrength * (populationBias * populationBias)), w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f, strength = (0.2f * ditherStrength / (populationBias * populationBias * populationBias * populationBias)), limit = 5f + 125f / (float)Math.sqrt(colorCount+1.5), dmul = 0x1p-8f; for (int y = 0; y < h; y++) { int ny = y + 1; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, y, 0); else { er = Math.min(Math.max(( ( (PaletteReducer.TRI_BLUE_NOISE [(px & 63) | (y & 63) << 6] + 0.5f) + ((((px+1) * 0xC13FA9A902A6328FL + (y +1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1p-16f - 0x1p+6f)) * strength) + (curErrorRed[px]), -limit), limit); eg = Math.min(Math.max(( ( (PaletteReducer.TRI_BLUE_NOISE_B[(px & 63) | (y & 63) << 6] + 0.5f) + ((((px+3) * 0xC13FA9A902A6328FL + (y -1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1p-16f - 0x1p+6f)) * strength) + (curErrorGreen[px]), -limit), limit); eb = Math.min(Math.max(( ( (PaletteReducer.TRI_BLUE_NOISE_C[(px & 63) | (y & 63) << 6] + 0.5f) + ((((px+2) * 0xC13FA9A902A6328FL + (y -4) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1p-16f - 0x1p+6f)) * strength) + (curErrorBlue[px]), -limit), limit); 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 = (dmul * ((color>>>24)- (used>>>24)) ); gdiff = (dmul * ((color>>>16&255)-(used>>>16&255))); bdiff = (dmul * ((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; } public Pixmap reduceWren(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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 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 partialDitherStrength = (0.4f * ditherStrength * (populationBias * populationBias)), strength = (40f * ditherStrength / (populationBias * populationBias)), blueStrength = (0.15f * ditherStrength / (populationBias * populationBias)), limit = 5f + 125f / (float)Math.sqrt(colorCount+1.5f), r1, g1, b1, r2, g2, b2, r4, g4, b4; for (int y = 0; y < h; y++) { System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int x = 0; x < lineLen; x++) { color = pixmap.getPixel(x, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(x, y, 0); else { er = Math.min(Math.max(( ( (PaletteReducer.TRI_BLUE_NOISE [(x & 63) | (y & 63) << 6] + 0.5f) * blueStrength + ((((x+1) * 0xC13FA9A902A6328FL + (y+1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-24f - 0x1.4p-2f) * strength)), -limit), limit) + (curErrorRed[x]); eg = Math.min(Math.max(( ( (PaletteReducer.TRI_BLUE_NOISE_B[(x & 63) | (y & 63) << 6] + 0.5f) * blueStrength + ((((x+3) * 0xC13FA9A902A6328FL + (y-1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-24f - 0x1.4p-2f) * strength)), -limit), limit) + (curErrorGreen[x]); eb = Math.min(Math.max(( ( (PaletteReducer.TRI_BLUE_NOISE_C[(x & 63) | (y & 63) << 6] + 0.5f) * blueStrength + ((((x+2) * 0xC13FA9A902A6328FL + (y-4) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-24f - 0x1.4p-2f) * strength)), -limit), limit) + (curErrorBlue[x]); 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(x, y, used); rdiff = ((color>>>24)- (used>>>24)) * partialDitherStrength; gdiff = ((color>>>16&255)-(used>>>16&255)) * partialDitherStrength; bdiff = ((color>>>8&255)- (used>>>8&255)) * partialDitherStrength; r1 = rdiff * 16f / (float)Math.sqrt(2048f + rdiff * rdiff); g1 = gdiff * 16f / (float)Math.sqrt(2048f + gdiff * gdiff); b1 = bdiff * 16f / (float)Math.sqrt(2048f + bdiff * bdiff); r2 = r1 + r1; g2 = g1 + g1; b2 = b1 + b1; r4 = r2 + r2; g4 = g2 + g2; b4 = b2 + b2; if(x < lineLen - 1) { curErrorRed[x+1] += r4; curErrorGreen[x+1] += g4; curErrorBlue[x+1] += b4; if(x < lineLen - 2) { curErrorRed[x+2] += r2; curErrorGreen[x+2] += g2; curErrorBlue[x+2] += b2; } } if(y+1 < h) { if(x > 0) { nextErrorRed[x-1] += r2; nextErrorGreen[x-1] += g2; nextErrorBlue[x-1] += b2; if(x > 1) { nextErrorRed[x-2] += r1; nextErrorGreen[x-2] += g1; nextErrorBlue[x-2] += b1; } } nextErrorRed[x] += r4; nextErrorGreen[x] += g4; nextErrorBlue[x] += b4; if(x < lineLen - 1) { nextErrorRed[x+1] += r2; nextErrorGreen[x+1] += g2; nextErrorBlue[x+1] += b2; if(x < lineLen - 2) { nextErrorRed[x+2] += r1; nextErrorGreen[x+2] += g1; nextErrorBlue[x+2] += b1; } } } } } } 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 = 60f * ditherStrength / (populationBias * OtherMath.cbrtPositive(colorCount)); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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 = Math.min(Math.max((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 = Math.min(Math.max((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 = Math.min(Math.max((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 (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 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; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 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; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, py); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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 = 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, 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; } /** * An error-diffusion dither that adds in error based on blue noise, much like {@link #reduceNeue(Pixmap)}, but * unlike Neue it adds different blue noise values in for each RGB channel. This tends to improve color accuracy * quite a bit, but does add some random-seeming noise from how the different noise textures aren't connected. For * some palettes, this may very well be the best dither here. It has different color quality when compared to * {@link #reduceWoven(Pixmap)}, and is sometimes better, while this dither lacks the repetitive artifacts in Woven. *
* This dither uses blue noise, and it's baseball season in America; my local LA Dodgers have blue as their color. * * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceDodgy(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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 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 = 25f * ditherStrength * populationBias * populationBias, w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f, strength = 0.25f * ditherStrength / (populationBias * populationBias), limit = 5f + 90f / (float)Math.sqrt(colorCount+1.5f), dmul = 0x1.8p-9f; for (int py = 0; py < h; py++) { int ny = py + 1; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, py); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, py, 0); else { er = Math.min(Math.max(((TRI_BLUE_NOISE [(px & 63) | (py & 63) << 6] + 0.5f) * strength), -limit), limit) + (curErrorRed[px]); eg = Math.min(Math.max(((TRI_BLUE_NOISE_B[(px & 63) | (py & 63) << 6] + 0.5f) * strength), -limit), limit) + (curErrorGreen[px]); eb = Math.min(Math.max(((TRI_BLUE_NOISE_C[(px & 63) | (py & 63) << 6] + 0.5f) * 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, py, used); rdiff = (dmul * ((color>>>24)- (used>>>24)) ); gdiff = (dmul * ((color>>>16&255)-(used>>>16&255))); bdiff = (dmul * ((color>>>8&255)- (used>>>8&255)) ); // rdiff /= (0.2f + Math.abs(rdiff)); // gdiff /= (0.2f + Math.abs(gdiff)); // bdiff /= (0.2f + 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; } /** * Burkes error diffusion dither with some extra error added in, selecting different types of error pattern in an * ordered way. This incorporates two types of extra error to each channel of each pixel, selecting based on a grid * of 2x2 pixel squares. Error applies differently to each RGB channel. The types of extra error are: *

    *
  • An R2 dither value (as used by {@link #reduceRoberts(Pixmap)}) is used for each pixel, but the four corners * of the 2x2 square each use a different angle for the artifacts.
  • *
  • Blue noise from {@link #TRI_BLUE_NOISE} is incorporated into two corners, with different strength.
  • *
  • XOR-Mod patterns are incorporated when blue noise isn't. They consist of diagonal lines. These are: *
      *
    • {@code ((px ^ y) % 9 - 4)}
    • *
    • {@code ((px ^ y) % 11 - 5)}
    • *
    *
  • *
*
* This is called Overboard because it is probably going overboard with the different types of extra error. Just * Burkes dither on its own is probably good enough. The results can look quite good, though, and tend to be * slightly smoother than {@link Dithered.DitherAlgorithm#WREN}. This also looks better than WREN when the dither * strength is higher than 1.0 and the color count is high. * * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceOverboard(Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); final float strength = ditherStrength * 0.5f * (populationBias * populationBias), noiseStrength = 2f / (populationBias), limit = 5f + 125f / (float)Math.sqrt(colorCount+1.5f); 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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); for (int y = 0; y < h; y++) { int ny = y + 1; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int x = 0; x < lineLen; x++) { int color = pixmap.getPixel(x, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(x, y, 0); else { float er = 0f, eg = 0f, eb = 0f; switch ((x << 1 & 2) | (y & 1)){ case 0: er += ((x ^ y) % 9 - 4); er += ((x * 0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL) >> 41) * 0x1p-20f; eg += (TRI_BLUE_NOISE_B[(x & 63) | (y & 63) << 6] + 0.5f) * 0x1p-5f; eg += ((x * -0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL) >> 41) * 0x1p-20f; eb += (TRI_BLUE_NOISE_C[(x & 63) | (y & 63) << 6] + 0.5f) * 0x1p-6f; eb += ((y * 0xC13FA9A902A6328FL + x * -0x91E10DA5C79E7B1DL) >> 41) * 0x1.8p-20f; break; case 1: er += (TRI_BLUE_NOISE[(x & 63) | (y & 63) << 6] + 0.5f) * 0x1p-5f; er += ((x * -0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL) >> 41) * 0x1p-20f; eg += (TRI_BLUE_NOISE_B[(x & 63) | (y & 63) << 6] + 0.5f) * 0x1p-6f; eg += ((y * 0xC13FA9A902A6328FL + x * -0x91E10DA5C79E7B1DL) >> 41) * 0x1.8p-20f; eb += ((x ^ y) % 11 - 5); eb += ((y * -0xC13FA9A902A6328FL + x * -0x91E10DA5C79E7B1DL) >> 41) * 0x1.8p-21f; break; case 2: er += (TRI_BLUE_NOISE[(x & 63) | (y & 63) << 6] + 0.5f) * 0x1p-6f; er += ((y * 0xC13FA9A902A6328FL + x * -0x91E10DA5C79E7B1DL) >> 41) * 0x1.8p-20f; eg += ((x ^ y) % 11 - 5); eg += ((y * -0xC13FA9A902A6328FL + x * -0x91E10DA5C79E7B1DL) >> 41) * 0x1.8p-21f; eb += ((x ^ y) % 9 - 4); eb += ((x * 0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL) >> 41) * 0x1p-20f; break; default: // case 3: er += ((x ^ y) % 11 - 5); er += ((y * -0xC13FA9A902A6328FL + x * -0x91E10DA5C79E7B1DL) >> 41) * 0x1.8p-21f; eg += ((x ^ y) % 9 - 4); eg += ((x * 0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL) >> 41) * 0x1p-20f; eb += (TRI_BLUE_NOISE_C[(x & 63) | (y & 63) << 6] + 0.5f) * 0x1p-5f; eb += ((x * -0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL) >> 41) * 0x1p-20f; break; } er = er * noiseStrength + curErrorRed[x]; eg = eg * noiseStrength + curErrorGreen[x]; eb = eb * noiseStrength + curErrorBlue[x]; int rr = Math.min(Math.max((int)(((color >>> 24) ) + Math.min(Math.max(er, -limit), limit) + 0.5f), 0), 0xFF); int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + Math.min(Math.max(eg, -limit), limit) + 0.5f), 0), 0xFF); int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + Math.min(Math.max(eb, -limit), limit) + 0.5f), 0), 0xFF); byte paletteIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))]; int used = paletteArray[paletteIndex & 0xFF]; pixmap.drawPixel(x, y, used); float rdiff = ((color >>> 24) - (used >>> 24)) * strength; float gdiff = ((color >>> 16 & 255) - (used >>> 16 & 255)) * strength; float bdiff = ((color >>> 8 & 255) - (used >>> 8 & 255)) * strength; float r1 = rdiff * 16f / (45f + Math.abs(rdiff)); float g1 = gdiff * 16f / (45f + Math.abs(gdiff)); float b1 = bdiff * 16f / (45f + Math.abs(bdiff)); // float r1 = rdiff * 16f / (float)Math.sqrt(2048f + rdiff * rdiff); // float g1 = gdiff * 16f / (float)Math.sqrt(2048f + gdiff * gdiff); // float b1 = bdiff * 16f / (float)Math.sqrt(2048f + bdiff * bdiff); float r2 = r1 + r1; float g2 = g1 + g1; float b2 = b1 + b1; float r4 = r2 + r2; float g4 = g2 + g2; float b4 = b2 + b2; if(x < lineLen - 1) { curErrorRed[x+1] += r4; curErrorGreen[x+1] += g4; curErrorBlue[x+1] += b4; if(x < lineLen - 2) { curErrorRed[x+2] += r2; curErrorGreen[x+2] += g2; curErrorBlue[x+2] += b2; } } if(ny < h) { if(x > 0) { nextErrorRed[x-1] += r2; nextErrorGreen[x-1] += g2; nextErrorBlue[x-1] += b2; if(x > 1) { nextErrorRed[x-2] += r1; nextErrorGreen[x-2] += g1; nextErrorBlue[x-2] += b1; } } nextErrorRed[x] += r4; nextErrorGreen[x] += g4; nextErrorBlue[x] += b4; if(x < lineLen - 1) { nextErrorRed[x+1] += r2; nextErrorGreen[x+1] += g2; nextErrorBlue[x+1] += b2; if(x < lineLen - 2) { nextErrorRed[x+2] += r1; nextErrorGreen[x+2] += g1; nextErrorBlue[x+2] += b1; } } } } } } pixmap.setBlending(blending); return pixmap; } /** * Modifies the given Pixmap so that it only uses colors present in this PaletteReducer, dithering when it can * with Burkes dithering, a type of error-diffusion dither. This method looks, surprisingly, quite a lot better * than Floyd-Steinberg dithering, despite being similar in most regards. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceBurkes (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float r4, r2, r1, g4, g2, g1, b4, b2, b1; final float s = 0.175f * ditherStrength * (populationBias * populationBias * populationBias), strength = s * 0.29f / (0.19f + s); 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); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); for (int py = 0; py < h; py++) { int ny = py + 1; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, lineLen); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, lineLen); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, lineLen); Arrays.fill(nextErrorRed, 0, lineLen, 0); Arrays.fill(nextErrorGreen, 0, lineLen, 0); Arrays.fill(nextErrorBlue, 0, lineLen, 0); for (int px = 0; px < lineLen; px++) { int color = pixmap.getPixel(px, py); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, py, 0); else { float er = curErrorRed[px]; float eg = curErrorGreen[px]; float 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); byte paletteIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))]; int used = paletteArray[paletteIndex & 0xFF]; pixmap.drawPixel(px, py, used); int rdiff = (color >>> 24) - (used >>> 24); int gdiff = (color >>> 16 & 255) - (used >>> 16 & 255); int bdiff = (color >>> 8 & 255) - (used >>> 8 & 255); r1 = rdiff * strength; g1 = gdiff * strength; b1 = bdiff * strength; r2 = r1 + r1; g2 = g1 + g1; b2 = b1 + b1; r4 = r2 + r2; g4 = g2 + g2; b4 = b2 + b2; if(px < lineLen - 1) { curErrorRed[px+1] += r4; curErrorGreen[px+1] += g4; curErrorBlue[px+1] += b4; if(px < lineLen - 2) { curErrorRed[px+2] += r2; curErrorGreen[px+2] += g2; curErrorBlue[px+2] += b2; } } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += r2; nextErrorGreen[px-1] += g2; nextErrorBlue[px-1] += b2; if(px > 1) { nextErrorRed[px-2] += r1; nextErrorGreen[px-2] += g1; nextErrorBlue[px-2] += b1; } } nextErrorRed[px] += r4; nextErrorGreen[px] += g4; nextErrorBlue[px] += b4; if(px < lineLen - 1) { nextErrorRed[px+1] += r2; nextErrorGreen[px+1] += g2; nextErrorBlue[px+1] += b2; if(px < lineLen - 2) { nextErrorRed[px+2] += r1; nextErrorGreen[px+2] += g1; nextErrorBlue[px+2] += b1; } } } } } } pixmap.setBlending(blending); return pixmap; } /** * A variant on {@link #reduceBurkes(Pixmap)} that multiplies the diffused error per-pixel using * {@link #TRI_BLUE_NOISE_MULTIPLIERS}. This does a good job of breaking up artifacts in sections * of flat color, where with Burkes, there could be ugly repetitive areas with seams. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceOceanic (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int w = pixmap.getWidth(), h = pixmap.getHeight(); final float[] noise = TRI_BLUE_NOISE_MULTIPLIERS; float r4, r2, r1, g4, g2, g1, b4, b2, b1; final float s = 0.175f * ditherStrength * (populationBias * populationBias * populationBias), strength = s * 0.29f / (0.19f + s); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(w)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(w)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(w)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(w)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(w)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(w)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(w); nextErrorRed = nextErrorRedFloats.ensureCapacity(w); curErrorGreen = curErrorGreenFloats.ensureCapacity(w); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(w); curErrorBlue = curErrorBlueFloats.ensureCapacity(w); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(w); Arrays.fill(nextErrorRed, 0, w, 0); Arrays.fill(nextErrorGreen, 0, w, 0); Arrays.fill(nextErrorBlue, 0, w, 0); } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used, rdiff, gdiff, bdiff; float er, eg, eb; byte paletteIndex; for (int py = 0; py < h; py++) { int ny = py + 1; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, w); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, w); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, w); Arrays.fill(nextErrorRed, 0, w, 0); Arrays.fill(nextErrorGreen, 0, w, 0); Arrays.fill(nextErrorBlue, 0, w, 0); for (int px = 0; px < w; px++) { color = pixmap.getPixel(px, py); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, py, 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); paletteIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))]; used = paletteArray[paletteIndex & 0xFF]; pixmap.drawPixel(px, py, used); rdiff = (color>>>24)- (used>>>24); gdiff = (color>>>16&255)-(used>>>16&255); bdiff = (color>>>8&255)- (used>>>8&255); r1 = rdiff * strength; g1 = gdiff * strength; b1 = bdiff * strength; r2 = r1 + r1; g2 = g1 + g1; b2 = b1 + b1; r4 = r2 + r2; g4 = g2 + g2; b4 = b2 + b2; float modifier; if(px < w - 1) { modifier = noise[(px + 1 & 63) | ((py << 6) & 0xFC0)]; curErrorRed[px+1] += r4 * modifier; curErrorGreen[px+1] += g4 * modifier; curErrorBlue[px+1] += b4 * modifier; if(px < w - 2) { modifier = noise[(px + 2 & 63) | ((py << 6) & 0xFC0)]; curErrorRed[px+2] += r2 * modifier; curErrorGreen[px+2] += g2 * modifier; curErrorBlue[px+2] += b2 * modifier; } } if(ny < h) { if(px > 0) { modifier = noise[(px - 1 & 63) | ((ny << 6) & 0xFC0)]; nextErrorRed[px-1] += r2 * modifier; nextErrorGreen[px-1] += g2 * modifier; nextErrorBlue[px-1] += b2 * modifier; if(px > 1) { modifier = noise[(px - 2 & 63) | ((ny << 6) & 0xFC0)]; nextErrorRed[px-2] += r1 * modifier; nextErrorGreen[px-2] += g1 * modifier; nextErrorBlue[px-2] += b1 * modifier; } } modifier = noise[(px & 63) | ((ny << 6) & 0xFC0)]; nextErrorRed[px] += r4 * modifier; nextErrorGreen[px] += g4 * modifier; nextErrorBlue[px] += b4 * modifier; if(px < w - 1) { modifier = noise[(px + 1 & 63) | ((ny << 6) & 0xFC0)]; nextErrorRed[px+1] += r2 * modifier; nextErrorGreen[px+1] += g2 * modifier; nextErrorBlue[px+1] += b2 * modifier; if(px < w - 2) { modifier = noise[(px + 2 & 63) | ((ny << 6) & 0xFC0)]; nextErrorRed[px+2] += r1 * modifier; nextErrorGreen[px+2] += g1 * modifier; nextErrorBlue[px+2] += b1 * modifier; } } } } } } pixmap.setBlending(blending); return pixmap; } /** * A variant on {@link #reduceOceanic(Pixmap)} (and thus on {@link #reduceBurkes(Pixmap)}) that uses * different blue noise effects per-channel, which can improve color quality at a minor speed cost. * This also makes an unorthodox change to the Burkes error diffusion pattern by diffusing a small amount of error * 3 pixels to the right. This is meant to break up some fine horizontal band artifacts. * * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceSeaside (Pixmap pixmap) { boolean hasTransparent = (paletteArray[0] == 0); final int w = pixmap.getWidth(), h = pixmap.getHeight(); final float[] noiseA = TRI_BLUE_NOISE_MULTIPLIERS; final float[] noiseB = TRI_BLUE_NOISE_MULTIPLIERS_B; final float[] noiseC = TRI_BLUE_NOISE_MULTIPLIERS_C; final float s = (0.13f * ditherStrength * (populationBias * populationBias)), strength = s * 0.29f / (0.18f + s); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(w)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(w)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(w)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(w)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(w)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(w)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(w); nextErrorRed = nextErrorRedFloats.ensureCapacity(w); curErrorGreen = curErrorGreenFloats.ensureCapacity(w); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(w); curErrorBlue = curErrorBlueFloats.ensureCapacity(w); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(w); Arrays.fill(nextErrorRed, 0, w, 0); Arrays.fill(nextErrorGreen, 0, w, 0); Arrays.fill(nextErrorBlue, 0, w, 0); } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int color, used, rdiff, gdiff, bdiff; float er, eg, eb; byte paletteIndex; for (int py = 0; py < h; py++) { int ny = py + 1; System.arraycopy(nextErrorRed, 0, curErrorRed, 0, w); System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, w); System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, w); Arrays.fill(nextErrorRed, 0, w, 0); Arrays.fill(nextErrorGreen, 0, w, 0); Arrays.fill(nextErrorBlue, 0, w, 0); for (int px = 0; px < w; px++) { color = pixmap.getPixel(px, py); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ pixmap.drawPixel(px, py, 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); paletteIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))]; used = paletteArray[paletteIndex & 0xFF]; pixmap.drawPixel(px, py, used); rdiff = (color>>>24)- (used>>>24); gdiff = (color>>>16&255)-(used>>>16&255); bdiff = (color>>>8&255)- (used>>>8&255); int modifier = ((px & 63) | (py << 6 & 0xFC0)); final float r1 = rdiff * strength * noiseA[modifier]; final float g1 = gdiff * strength * noiseB[modifier]; final float b1 = bdiff * strength * noiseC[modifier]; final float r2 = r1 + r1; final float g2 = g1 + g1; final float b2 = b1 + b1; final float r4 = r2 + r2; final float g4 = g2 + g2; final float b4 = b2 + b2; if(px < w - 1) { modifier = ((px + 1 & 63) | (py << 6 & 0xFC0)); curErrorRed[px+1] += r4 * noiseA[modifier]; curErrorGreen[px+1] += g4 * noiseB[modifier]; curErrorBlue[px+1] += b4 * noiseC[modifier]; if(px < w - 2) { modifier = ((px + 2 & 63) | ((py << 6) & 0xFC0)); curErrorRed[px+2] += r2 * noiseA[modifier]; curErrorGreen[px+2] += g2 * noiseB[modifier]; curErrorBlue[px+2] += b2 * noiseC[modifier]; } if(px < w - 3) { modifier = ((px + 3 & 63) | ((py << 6) & 0xFC0)); curErrorRed[px+2] += r1 * noiseA[modifier]; curErrorGreen[px+2] += g1 * noiseB[modifier]; curErrorBlue[px+2] += b1 * noiseC[modifier]; } } if(ny < h) { if(px > 0) { modifier = (px - 1 & 63) | ((ny << 6) & 0xFC0); nextErrorRed[px-1] += r2 * noiseA[modifier]; nextErrorGreen[px-1] += g2 * noiseB[modifier]; nextErrorBlue[px-1] += b2 * noiseC[modifier]; if(px > 1) { modifier = (px - 2 & 63) | ((ny << 6) & 0xFC0); nextErrorRed[px-2] += r1 * noiseA[modifier]; nextErrorGreen[px-2] += g1 * noiseB[modifier]; nextErrorBlue[px-2] += b1 * noiseC[modifier]; } } modifier = (px & 63) | ((ny << 6) & 0xFC0); nextErrorRed[px] += r4 * noiseA[modifier]; nextErrorGreen[px] += g4 * noiseB[modifier]; nextErrorBlue[px] += b4 * noiseC[modifier]; if(px < w - 1) { modifier = (px + 1 & 63) | ((ny << 6) & 0xFC0); nextErrorRed[px+1] += r2 * noiseA[modifier]; nextErrorGreen[px+1] += g2 * noiseB[modifier]; nextErrorBlue[px+1] += b2 * noiseC[modifier]; if(px < w - 2) { modifier = (px + 2 & 63) | ((ny << 6) & 0xFC0); nextErrorRed[px+2] += r1 * noiseA[modifier]; nextErrorGreen[px+2] += g1 * noiseB[modifier]; nextErrorBlue[px+2] += b1 * noiseC[modifier]; } } } } } } pixmap.setBlending(blending); return pixmap; } /** * 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 requires items to be present as two ints per item: the * earlier int, at an index less than 16, is what gets sorted, while the later int, at an index 16 higher than the * earlier one, is an RGB555 int. *
* 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 RGB555 colors in the later half of {@code ints}. * @param ints an int array than must be able to take a, b, a+16, and b+16 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 * 0.5f / populationBias); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { color = pixmap.getPixel(px, y); if (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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 (hasTransparent && (color & 0x80) == 0) /* if this pixel is less than 50% opaque, draw a pure transparent pixel. */ 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 fewer 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 - 2025 Weber Informatics LLC | Privacy Policy