А давайте я вам расскажу про градиенты!


    скрин финального результата

    В этой статье я рассказываю о том, как я изобрел свой личный велосипед, рисующий градиент практически как в фотошопе. Сразу предупреждаю, алгоритм ужасно медленный и неоптимизированный. Оптимизацией и рассмотрением какого-нибудь популярного алгоритма градиента я собираюсь заняться во второй части статьи

    Зачем?


    Как-то захотелось мне реализовать программную отрисовку градиентов, максимально похожих на фотошоповские. Никакой конкретной цели у меня не было, так, интересная задачка на вечер. В качестве языка была выбрана Java. Важной идеей было то, что я хотел написать этот алгоритм именно своими силами, не подглядывая в чужие алгоритмы.


    Что должно было получиться

    Метод drawGradient() должен работать следующим образом: мы задаем координаты и цвета двух точек, после чего поверх всего изображения рисуется градиент. Примерно так:


    На данном рисунке точка A имеет координаты (55; 20) и цвет 0xff2e2e2e, а точка B — координаты (175; 180) и цвет 0xffb5b5b5. не забываем, что начало координат находится в левом верхнем углу, а ось Y направлена вниз.

    Начинаем разбираться

    В качестве эталона я взял градиент из фотошопа с прошлого скриншота. Как можно заметить, градиент состоит из трех частей:

    Красная часть должна быть залита цветом точки А, зеленая — точки B, а цвет каждого пикселя оставшейся зоны должен вычисляться в зависимости от расстояния от него до прямых c и d.

    Думаю очевидно, что нам нужен алгоритм, который будет определять расстояние от любого пикселя до прямых c и d. Также нужен способ для определения того, какой пиксель лежит в «красной» области, какой в «зеленой», а какой в оставшейся области.

    Вспомним школьный курс геометрии и нарисуем следующую иллюстрацию для случайного пикселя E:


    На данном рисунке AF — расстояние от пикселя E до прямой a. И, соответсвенно, FB — расстояние до прямой b. Именно эти расстояния и будут определять цвет пикселя. И тут же решается и проблема с определением того, к какой области относится пиксель. Тут все очень просто. Если AF + FB > AB, значит пиксель лежит либо в красной, либо в зеленой зоне. Чтобы определить в какой именно, сравним AF и FB. Если AF > FB, значит пиксель лежит в зеленой зоне, иначе в красной. Вот такая математика.

    Итак, наша задача найти AF и BF. Сконцентрируемся на AF, по теореме Пифагора выходит, что:


    Так, квадрат длины AE мы можем узнать из той же теоремы Пифагора, благо нам известны координаты точек A и E. Получается вот так:


    Осталось найти только EF. Это немного потруднее, но ничего страшного. Так как наш отрезок EF представляет собой высоту треугольника, опущенную на сторону AB, нам поможет формула нахождения высоты. Выглядит она так:


    А p — это одна из самых вводящих в заблуждение вещей. Это не периметр, а полупериметр. Помню пару раз совершал в школе фэйлы по этому поводу. Считается так:


    AB и EB подсчитываем точно также как и AE — исходя из координат.

    Итак, алгоритм подсчета AF как на ладони:
    1. Рассчитываем AE, EB и AB
    2. Рассчитываем p
    3. Рассчитываем EF
    4. Рассчитываем AF

    Алгоритм для BF аналогичен, не буду его расписывать.

    Самое время покодить!

    Я решил создать класс, который представляет обертку для BufferedImage, назовем его EditableImage. И в этом классе, по моим прикидкам, должны были быть следующие методы:
    	EditableImage(int width, int height); //Конструктор
    
    	void clear(int color); //Сброс всех пикселей изображения в заданный цвет
    
    	void drawGradient(int x1, int y1, int color1, int x2, int y2, int color2); //Сам метод отрисовки градиентов
    
    	BufferedImage getImage(); //Метод для получения получившегося изображения
    

    Тут и далее все цвета в моем коде задаются в виде int, который имеет следующий вид:
    0xAARRGGBB
    AA - значение прозрачности (у нас будут 32х битные картинки с прозрачностью)
    RR - значение красного цвета
    GG - значение зеленого цвета
    BB - значение синего цвета
    

    Идея с классом удобна тем, что если я потом захочу реализовать еще какие-нибудь фишки кроме градиента, это будет легко сделать.

    Начнем со вспомогательных штук, которые к градиенту не имеют отношения
    Gradient.java - точка входа программы
    package ru.idgdima.gradient;
    
    import javax.swing.*;
    
    public class Gradient {
    	public static final int IMG_WIDTH = 640;
    	public static final int IMG_HEIGHT = 480;
    
    	private static GradientPanel panel; //Панель, которая будет рисовать наш градиент
    
    	public static void main(String[] args) {
    		//Создаем окно, заставляем его закрывать программу при нажатии на крест и
    		//запрещаем масштабирование окна
    		JFrame frame = new JFrame("Test");
    		frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    		frame.setResizable(false);
    
    		//Создаем нашу панель для отрисовки градиента, добавляем ее в окно,
    		//подгоняем его размер под размер панели, размещаем окно по центру
    		//экрана и делаем его видимым
    		panel = new GradientPanel(IMG_WIDTH, IMG_HEIGHT);
    		frame.add(panel);
    		frame.pack();
    		frame.setLocationRelativeTo(null);
    		frame.setVisible(true);
    	}
    }
    


    GradientPanel.java - панель, на которой будет отображаться градиент
    package ru.idgdima.gradient;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.image.BufferedImage;
    
    public class GradientPanel extends JPanel {
    	private BufferedImage image;
    
    	public GradientPanel(int width, int height) {
    		//Вызываем конструктор родителя и устанавливаем размер панели
    		super();
    		setPreferredSize(new Dimension(width, height));
    
    		//Создаем изображение, заполняем его черными пикселями, рисуем 
    		//на нем градиент и сохраняем его в качестве BufferedImage в поле
    		//класса для дальнейшей отрисовки
    		EditableImage gradientImage = new EditableImage(width, height);
    		gradientImage.clear(0xff000000);
    		gradientImage.drawGradient(55, 20, 0xff2e2e2e, 175, 180, 0xffb5b5b5);
    		image = gradientImage.getImage();
    	}
    
    	@Override
    	protected void paintComponent(Graphics g) {
    		super.paintComponent(g);
    
    		//В этом методе происходит отрисовка панели. Просто рисуем наше изображение
    		//поверх нее:
    		g.drawImage(image, 0, 0, null);
    	}
    }
    


    EditableImage.java - Самый интересный класс, метод для градиента пока пуст
    package ru.idgdima.gradient;
    
    import java.awt.image.BufferedImage;
    
    public class EditableImage {
    	private int width;
    	private int height;
    	private int[] rgb; //все пиксели картинки будет храниться в этом массиве
    	BufferedImage image; //этот объект используется только для возвращения методом getImage
    
    	public EditableImage(int width, int height) {
    		this.width = width;
    		this.height = height;
    		rgb = new int[width * height]; //создаем массив достаточных размеров
    		//также создаем картинку для возвращения из метода getImage
    		image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    	}
    
    	/**
    	 * Метод устанавливает всем пикселям изображения одинаковый цвет
    	 * @param color желаемый цвет
    	 */
    	public void clear(int color) {
    		for (int i = 0; i < rgb.length; i++) {
    			rgb[i] = color;
    		}
    	}
    
    	/**
    	 * Метод возвращает текущее изображение
    	 * @return
    	 */
    	public BufferedImage getImage() {
    		//Копируем пиксели из массива rgb в image
    		image.setRGB(0, 0, width, height, rgb, 0, width);
    		return image;
    	}
    
    	public void drawGradient(int x1, int y1, int color1,
    			int x2, int y2, int color2) {
    		//пока пусто
    	}
    }
    



    Данную программу можно скомпилировать уже на этом этапе, но она покажет нам просто черную картинку. Самое время написать метод для градиента!

    У меня получилось вот так:
    drawGradient
    public void drawGradient(int x1, int y1, int color1, int x2, int y2, int color2) {
    	float dx = x1 - x2; //Вспомогательные переменные
    	float dy = y1 - y2;
    	float AB = (float) Math.sqrt(dx * dx + dy * dy); //Вычисляется только один раз
    
    	//Перебираем все пиксели изображения
    	for (int y = 0; y < height; y++) {
    		for (int x = 0; x < width; x++) {
    			dx = x1 - x;
    			dy = y1 - y;
    			float AE2 = dx * dx + dy * dy;
    			float AE = (float) Math.sqrt(AE2);
    
    			dx = x2 - x;
    			dy = y2 - y;
    			float EB2 = dx * dx + dy * dy;
    			float EB = (float) Math.sqrt(EB2);
    
    			float p = (AB + AE + EB) / 2f;
    
    
    			float EF = 2 / AB * (float)Math.sqrt(Math.abs(p * (p - AB) *
    				(p - AE) * (p - EB)))
    			float EF2 = EF * EF;
    
    			float AF = (float) Math.sqrt(Math.abs(AE2 - EF2));
    			float BF = (float) Math.sqrt(Math.abs(EB2 - EF2));
    
    			if (AF + BF - 0.1f > AB) {
    				//Если пиксель лежит в зеленой или красной зоне
    				rgb[y * width + x] = AF < BF ? color1 : color2;
    			} else { //Рассчитываем цвет пикселя
    				float progress = AF / AB;
    				//Об методе interpolate речь пойдет дальше
    				rgb[y * width + x] = interpolate(color1, color2, progress);
    			}
    		}
    	}
    }
    
    /**
     * @param num - число
     * @return 0, если num < 0; 255, если num > 255; в остальных случаях num
     */
    private static int clip(int num) {
    	return num <= 0 ? 0 : (num >= 255 ? 255 : num);
    }
    



    Обратите внимание что я применяю функцию модуля — Math.abs() везде, где есть хоть малейшая вероятность того, что в функцию нахождения квадратного корня — Math.sqrt() может попасть отрицательное число. В противном случае у нас появятся артефакты.

    А в этой строке если убрать - 0.1f, получается просто ужасное месиво. Из-за погрешности вычислений нам приходится отнимать небольшое число:
    if (AF + BF - 0.1f > AB) {
    


    Осталось только разобраться с методом interpolate и дело в шляпе. Он должен принимать начальный цвет, конечный цвет и число progress, которое может быть от 0 до 1 и которое определяет долю каждого цвета. Например если progress = 0, возвращается начальный цвет, если progress = 1 — конечный цвет, а если progress = 0.5 — средний цвет между начальным и конечным. Задача ясна, метод написан:
    interpolate
    private int interpolate(int color1, int color2, float progress) {
    	//Разделяем оба цвета на составляющие
    	int a1 = (color1 & 0xff000000) >>> 24;
    	int r1 = (color1 & 0x00ff0000) >>> 16;
    	int g1 = (color1 & 0x0000ff00) >>> 8;
    	int b1 = color1 & 0x000000ff;
    
    	int a2 = (color2 & 0xff000000) >>> 24;
    	int r2 = (color2 & 0x00ff0000) >>> 16;
    	int g2 = (color2 & 0x0000ff00) >>> 8;
    	int b2 = color2 & 0x000000ff;
    
    	//И рассчитываем новые
    	float progress2 = (1 - progress);
    	int newA = clip((int) (a1 * progress2 + a2 * progress));
    	int newR = clip((int) (r1 * progress2 + r2 * progress));
    	int newG = clip((int) (g1 * progress2 + g2 * progress));
    	int newB = clip((int) (b1 * progress2 + b2 * progress));
    
    	//Собираем и возвращаем полученный цвет
    	return (newA << 24) + (newR << 16) + (newG << 8) + newB;
    }
    



    Давайте глянем на результат!



    Уже неплохо, но наш использует линейную интерполяцию, а в фотошопе в ходу определенно какая-то другая.

    Про интерполяции

    Посмотрите внимательно на картинку. Левый градиент нарисован нашим алгоритмом, правый — фотошопом. На каждой строке стоит красная точка. И чем темнее строка, тем точка левее:

    Как видно, линия у нас прямая, как рельса. Надо это исправлять. Вот тут я так ничего дельного не придумал и решил подсмотреть в интернете. Нашел статью на хабре, в которой описаны некоторые виды интерполяции и даже код есть: habrahabr.ru/post/142592

    Ну что, реализуем косинусную интерполяция! Во первых добавим в класс EditableImage две константы:
    public static final int INTERPOLATION_LINEAR = 0;
    public static final int INTERPOLATION_COS = 1;
    

    Во вторых перепишем немного метод interpolate, чтобы ему на вход подавалась одна из этих констант:
    Скрытый текст
    private int interpolate(int color1, int color2, float progress, int interpolation) {
    	//Разделяем оба цвета на составляющие
    	int a1 = (color1 & 0xff000000) >>> 24;
    	int r1 = (color1 & 0x00ff0000) >>> 16;
    	int g1 = (color1 & 0x0000ff00) >>> 8;
    	int b1 = color1 & 0x000000ff;
    
    	int a2 = (color2 & 0xff000000) >>> 24;
    	int r2 = (color2 & 0x00ff0000) >>> 16;
    	int g2 = (color2 & 0x0000ff00) >>> 8;
    	int b2 = color2 & 0x000000ff;
    
    	//И рассчитываем новые
    	float f;
    	if (interpolation == INTERPOLATION_LINEAR) {
    		f = progress;
    	} else if (interpolation == INTERPOLATION_COS) {
    		float ft = progress * 3.1415927f;
    		f = (1 - (float) Math.cos(ft)) * 0.5f;
    	} else {
    		throw new IllegalArgumentException();
    	}
    	int newA = clip((int) (a1 * (1 - f) + a2 * f));
    	int newR = clip((int) (r1 * (1 - f) + r2 * f));
    	int newG = clip((int) (g1 * (1 - f) + g2 * f));
    	int newB = clip((int) (b1 * (1 - f) + b2 * f));
    
    	//Собираем и возвращаем полученный цвет
    	return (newA << 24) + (newR << 16) + (newG << 8) + newB;
    }
    


    Затем в список параметров метода drawGradient добавим int interpolation и в строчку вызова метода interpolate добавим эту переменную:
    rgb[y * width + x] = interpolate(color1, color2, progress , interpolation);
    

    И в завершение в классе GradientPanel перепишем вызов метода drawGradient, добавив в него INTERPOLATION_COS

    Вот что получилось с этим методом интерполяции:

    Хм, выглядит неплохо, но в фотошопе линия явно не такая кривая. Что же делать? То слишком прямая, то слишком кривая… А что если сделать среднее из этих двух крайностей?

    Отличная идея, добавляем константу INTERPOLATION_COS_LINEAR = 2

    А в код метода interpolate добавляем еще один else if:
    } else if (interpolation == INTERPOLATION_COS_LINEAR) {
    	float ft = progress * 3.1415927f;
    	f = (progress + (1 - (float) Math.cos(ft)) * 0.5f) / 2f;
    }
    

    И о чудо, получилась практически полная копия градиента из фотошопа!
    Сами поглядите:

    Наша слева.

    А вот вам две картинки с верхнего скриншота, совмещенные в одну. Видно, что интерполяция практически одинаковая, отличия можно свалить на погрешности при округлениях:


    Ура, мы сделали градиент как в фотошопе, хоть и гораздо более медленный.
    Исходники и jar

    На этом я заканчиваю первую часть. Во второй части я постараюсь оптимизировать алгоритм так круто, как только смогу. И реализую какой-нибудь готовый известный быстрый алгоритм рисования градиента, а потом сравню его по скорости со своим.

    Кстати, если у вас есть на примете быстрые алгоритмы рисования градиентов, оставляйте их в комментариях.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 27

      0
      Не увлекаюсь явой, но статья, как ни странно, показалась интересной. Видимо, тяготит меня к «своим велосипедам». Даже подожду вторую часть.
        +2
        Джавой она называется, джавой.
        +1
        Спасибо за статью. Хороший велосипед, горный годный.
        Дрожь берёт, как представишь, сколько суровой математики скрыто за тем, чтобы «зделать симпотичную аватарку».
          +8
          Я бы вспомнил аналитическую геометрию (длина AF — проекция AE на AB), а еще лучше матричные преобразования (найти такую матрицу M, чтобы M*A = [0;0; 1], M*B=[0; 255; 1], тогда цвет пикселя будет color(координата_x(M*координаты_пикселя)), где f функция x->цвет). Так, сходу, никогда градиентов не рисовал.
            +7
            Ага. Автору стоит подучить векторную алгебру, через неё такие вещи гораздо проще делаются.
              0
              Поддерживаю. В данном случае AF можно найти как скалярное произведение векторов AE и AB, что значительно быстрее. Длину вектора AB можно посчитать заранее, причём лучше инвертированную, то есть 1 / |AB|. В итоге расчёт для каждой точки будет состоять из двух вычитаний (для нахождения вектора AE), двух умножений и сложения (скалярное произведение) и умножения (для вычисления отношения AF / AB).

              Более того, можно заметить, что линии, поперечные AB, имеют одинаковую лестничную структуру. Можно вычислить эту структуру и делать прогоны параллельными линиями.

              Короче, оптимизировать можно долго. :-)
                0
                А нормализация АE куда делась?
                  0
                  Верно, забыл. Нормализируем на длину вектора AB, поэтому посчитанный коэффициент можно возвести в квадрат. Исходя из этого, формула для него становится ещё проще: k = 1 / ( |AB| )^2 = 1 / (dx^2 + dy^2).
            +4
            О, благодаря этой статье я вспомнил свои увлечения градиентами в школе, даже с такой программой на областном конкурсе Малой академии наук второе место взял.
            Могу вам сходу подсказать приличную оптимизацию
            Описание оптимизации
            Вычерчивать такой градиент (а соответственно и вычислять цвета точек) нужно не по точкам, а целыми линиями, перпендикулярными отрезку, соединяющему управляющие точки градиента. Ведь для каждой такой линии точек одинаковый. Таким образом вы в десятки-сотни раз (смотря какая форма закрашиваемой области) уменьшите количество корней, косинусов и умножений.
            А т.к. все линии параллельны, и полностью заполняют собой пространство, рисовать можно без антиэлейзинга, и даже дельту линии нужно будет вычислить всего один раз.
            А в качестве начальных точек каждой линии брать точки на отрезке, соединяющем управляющие точки. Тогда и вычислением проекции стороны треугольника на основание заниматься не придется.
            По моим прикидкам ускорение должно быть раз в 50.

            Кстати, косинусная интерполяция — это просто замечательно. В свое время я так и не понял, почему мои градиенты «немного не такие» :)
              +12
              На курсе высшей математики при изучении градиентов у многих фотошоперов был разрыв шаблона, потому как градиенты были в виде уравнений и стрелочек и без графики!
                +2
                наш использует линейную интерполяцию, а в фотошопе в ходу определенно какая-то другая.

                В ФШ этим делом можно управлять через параметр Smoothness, по умолчанию равный 100%. Если установить его в 0% — получим просто линейный градиент. Разумеется, возможны и промежуточные значения.
                Это сделано для гладкости стыковки двух и более градиентов. При линейной интерполяции такие стыки становятся резко видны.
                Подробности с картинками, например, здесь: bourt.com/blog/?p=700

                Лично я предпочитаю это сглаживание включать только при необходимости. Если градиент одиночный — отключаю. Так получается естественнее и предсказуемее при экспорте, например, в CSS.
                  0
                  Спасибо, не знал. В принципе, если в ФШ при значении 0 получается результат как у меня с INTERPOLATION_LINEAR, а при значении 100 — как с INTERPOLATION_COS_LINEAR, можно легко реализовать передачу в метод параметра smoothness. Более того, можно ограничить его не сотней процентов, а двумя сотнями (INTERPOLATION_COS эквивалентен smoothness 200%, как я понимаю)
                    0
                    Скорее всего там используется не интерполяция косинусом (она не гибка, и неудобна в использовании) а полином третьей степени. См. мой комментарий ниже, и параметры P1 и P2 вычиляются в зависимости от smoothnes скорее всего так:
                    P1 = smoothnes * 0.5
                    P2 = 1 — smoothnes * 0.5

                    p.s. В фотошоп не заглядывал, нужно проверить
                      +1
                      ошибся, P1 и P2 надо считать так:
                      p1 := (1 — smooth) * (1 / 3);
                      p2 := (1 — p1);
                      а вот и семпл с исходником, клик мышом и тянем. Вверху трекбар — smoothnes как в фш: screenup.org/tmp/Grad.7z (осторожно, Delphi, но бинарник присутствует)
                    0
                    Простите мое невежество (далек от веб-графики). А что, из ФШ можно экспортировать градиенты в CSS, или под экспортом имеется ввиду нечто иное?
                    Спасибо за ответ!
                      +2
                      Я «экспортирую» вручную: просто беру пипеткой 2 крайних цвета и вставляю из в онлайн генератор www.colorzilla.com/gradient-editor/. Насчет автоматизации — не знаю, но вполне возможно, что таковые плагины есть.

                      Если параметр Smoothness=0, результат в CSS получается а) идентичным б) более плавным. При Smoothness=100 градиент, перенесенный в CSS по 2-ум крайним точкам, будет визуально более «плоский», чем казался в ФШ.

                        0
                        Спасибо за ответ!
                    –2
                    Учитывая наличие java.awt.GradientPaint, java.awt.LinearGradientPaint и java.awt.RadialGradientPaint, практическая значимость не очень ясна. Вот разве что была бы версия для SWT, так как там возможности рисования градиентов скромные.
                      +4
                      Можно я вам подскажу.
                      Вспомним что такое скалярное произведение векторов.
                      Это модуль одного вектора, на модуль другого и на косинус угла между ними: c = |a|*|b|*cos(ab). Так же справедлива формула: c = ax * bx + ay * by
                      Так вот, если косинус отрицательный, то угол больше 0.5*Пи, длины векторов у нас всегда положительны, а значит, что если все скалярное произведение отрицательно, то угол между векторами больше 0.5*Пи
                      По этому рисунку получается, что точка точка лежит в красной зоне, если (AE*AB) отрицательное.
                      Точка лежит в зеленой части, если (BE*BA) отрицательное.
                      image
                      И не надо никаких теорем пифагора, квадратных корней и прочего.
                      В сухом остатке, у вас есть координаты A (Ax. Ay), координаты B (Bx, By) и координаты E (Ex, Ey)
                      Проверки:
                      if (((Ex — Ax)*(Bx — Ax) + (Ey — Ay)*(By — Ay))<0) //точка в красной зоне
                      if (((Ex — Bx)*(Ax — Bx) + (Ey — By)*(Ay — By))<0) //точка в зеленой зоне

                      Теперь посмотрим, как поможет нам скалярное произведение для промежуточного этапа.
                      Вспоминаем, что косинус на тригонометрическом круге — ничто иное, как проекция на ось X. Т.к. у нас косинус между векторами, то можно считать, что ось X направлена так же, как наш вектор AB.
                      Смотрим еще раз на формулы скалярного произведения:
                      c = ax * bx + ay * by
                      c = |a|*|b|*cos(ab)
                      косинус отсюда будет:
                      cos(ab) = (ax * bx + ay * by) / (|a|*|b|)
                      если мы умножим длину вектора AE на этот косинус, то мы получим длину вектора AF
                      Но сам по себе вектор AF нам не удобен, для градиента. Удобно было бы получить нормализованное значение. Это значение, лежащее в диапазоне от 0 до 1, так, что если F лежит в точке A, то наше нормализованное значение будет 0, а если А лежит в точке B — то нормализованное значение будет равно 1, и значение изменяется линейно. Для этого нам нужно разделить длинну AF на длинну AB.
                      Итак финальный результат, зная координаты трех точек (как уже было описано ранее)
                      t = ((Ex — Ax)*(Bx — Ax) + (Ey — Ay)*(By — Ay)) / (|AB|*|AE|) * |AE| / |AB|
                      длина AE сокращается с длиной в знаменателе, длина AB превращается в квадрат длины вектора AB: ((Bx — Ax) * (Bx — Ax) + (By — Ay) * (By — Ay))
                      Причем квадрат длины AB можно подсчитать 1 раз.
                      Итак t = ((Ex — Ax)*(Bx — Ax) + (Ey — Ay)*(By — Ay)) / ((Bx — Ax) * (Bx — Ax) + (By — Ay) * (By — Ay))

                      Таким образом на точку нам понадобится считать максимум:
                      два скалярных произведения
                      ((Ex — Ax)*(Bx — Ax) + (Ey — Ay)*(By — Ay))
                      ((Ex — Bx)*(Ax — Bx) + (Ey — By)*(Ay — By)
                      и одно деление на квадрат длины AB

                      Что намного быстрее тех корней что у вас ;)
                        +1
                        Интерполяция.
                        Я не смотрел, как фотошоп рисует график, но судя по вашим картинкам нужна функция f(t)=t'

                        Для конкретного случая нам подойдет полином третьей степени, и я бы воспользовался кривой безье.
                        image
                        P0 у нас всегда 0. P3 у нас всегда 1. Ну а P1 и P2 либо на глазок, скажем P1 (0.3, 0.2) и P2 (0.7, 0.8) либо анализировать градиент фотошопа, чтобы сделать точную реплику
                        0
                        Думаю, вам будет очень интересно это видео, там как раз рассматриваются интерполяции градиентов: www.youtube.com/watch?v=BkTiqe-8QZA
                        Реализовав данные идеи, можно будет легко посоревноваться в качестве отрисовки градиентов со стандартной java-библиоекой.
                          +1
                          Хорошая статья! Куча инфы в комментариях!
                            0
                            Добавлю к градиентам:
                            rectangleworld.com/blog/archives/713

                            Правда там не Java.
                              +2
                              Еще нужно реализовать дизеринг, иначе градиент будет полосками (banding), особенно на некоторых дисплеях.
                                0
                                Спасибо за статью! Она как раз кстати — утром делал графику в джаве для диплома и тоже реализовал градиент, но другого типа. Интересное совпадение вышло.
                                Сейчас, благодаря Вашей статье, кое что подправил, ибо отрисовка изображения у меня было организована через… А впрочем, это уже совсем другая история.
                                  +1
                                  Много оптимизаций для алгоритма можно почерпнуть из компьютерной графики. В случае, приведенном у вас на пояснительных изображениях — каждая строка средней части градиента — это, по сути, градиент в одну линию. Промежуточные точки которого рассчитываются довольно быстро по крайним значениям, либо по начальному значению + смещение цвета на пиксель. При этом для последовательного расчета промежуточных точек будет использоваться только сложение. Естественно, всё это справедливо без наложения корректирующей кривой.
                                    0
                                    Недавно у меня вывелась формула, которая очень хорошо (разница не превышает 1%) приближает косинус-интерполяцию: 3 * t^2 — 2 * t^3

                                    Погуглил — оказывается, я переизобрел так называемый smoothstep, который давным-давно известен и реализован во всех графических библиотеках: en.wikipedia.org/wiki/Smoothstep

                                    Only users with full accounts can post comments. Log in, please.