음악, 삶, 개발

Juce 로 만드는 GUI 파트5 - ColourGradient 클래스 본문

개발 공부/Juce 공부방

Juce 로 만드는 GUI 파트5 - ColourGradient 클래스

Lee_____ 2020. 10. 28. 03:06

 

 

< ColourGradient 클래스 >

 

우리가 디자인툴에서 표현하는 Gradient 색상을 표현해줄수있는 클래스이다.

Graphics::setGradientFill 의 인자로 이 ColourGradient 객체를 넘기게 된다.

ColourGradient 객체를 잘 만들기위해선, 우선적으로 Gradient 에 대한 이해가 필수적이므로,

Adobe Illustrator 를 통해 이를 설명할것이다.

앞서 말했듯이, 디자인적인 결정은 디자인툴에서 해야하므로

Adobe Illustrator 에서 Gradient 색을 결정하고, 어떻게 Juce 코드로 이를 옮길수있는지에 집중할것이다.


< 공식 문서 >

 

ColourGradient Class Reference

 

Describes the layout and colours that should be used to paint a colour gradient.

 

See also Graphics::setGradientFill

/* Constructor */
ColourGradient ()                                                                                       noexcept
ColourGradient (const ColourGradient& )
ColourGradient (ColourGradient&& )                                                                      noexcept
ColourGradient (Colour colour1, float x1, float y1, Colour colour2, float x2, float y2, bool isRadial)
ColourGradient (Colour colour1, Point<float> point1, Colour colour2, Point<float> point2, bool isRadial)

/* Destructor */
~ColourGradient ()
 
/* Operator */
ColourGradient & 	operator=   (const ColourGradient& )
ColourGradient & 	operator=   (ColourGradient&& )               noexcept
bool 	            operator==  (const ColourGradient& )    const noexcept
bool 	            operator!=  (const ColourGradient& )    const noexcept

/* Get */
int 	getNumColours       ()                  const noexcept
double 	getColourPosition   (int index)         const noexcept
Colour 	getColour           (int index)         const noexcept
Colour 	getColourAtPosition (double position)   const noexcept
bool 	isOpaque            ()                  const noexcept
bool 	isInvisible         ()                  const noexcept


/* Set */
void 	setColour           (int index, Colour newColour)                                                   noexcept
void 	clearColours        ()
void 	removeColour        (int index)
void 	multiplyOpacity     (float multiplier)                                                              noexcept
int 	addColour           (double proportionAlongGradient, Colour colour)

/* ?? */
int 	createLookupTable   (const AffineTransform& transform, HeapBlock<PixelARGB>& resultLookupTable)     const
void 	createLookupTable   (PixelARGB* resultLookupTable, int numEntries)                                  const noexcept


/* Static Public Member Functions */
static ColourGradient   vertical    (Colour colour1, float y1, Colour colour2, float y2)
static ColourGradient   horizontal  (Colour colour1, float x1, Colour colour2, float x2)
template<typename Type>
static ColourGradient   vertical    (Colour colourTop, Colour colourBottom, Rectangle<Type> area)
template<typename Type>
static ColourGradient   horizontal  (Colour colourLeft, Colour colourRight, Rectangle<Type> area)

/* Public Attributes */
Point<float> point1
Point<float> point2
bool isRadial

< Gradient 란 ? >

일러스트레이터의 와 Gradient 패널

Gradient 란 두 지점간에 색 변화를 점진적으로 사용하는 색 표현법이다.

위의 Gradient 패널을 보면 2개의 지점을 기준으로, 색이 한쪽 방향으로 점차 변화하는것을 볼수있다.

Gradient 의 개념을 이해했다면, 어떠한 Gradient 의 종류가 있는지와,

각 종류마다 어떠한 값을 사용하는지 알아야한다. (우리는 결국 이를 코딩해야하므로)


< Gradient 의 종류 1 : Linear Gradient (선형 그라디언트)  >

 

Linear Gradient가장 일반적으로 많이 사용되는 형태이다.

Linear Gradient 라 부르는 이유는 색이 시작지점의 색으로부터 끝지점의 색으로 Linear 하게 변화되기때문이다.

 

위의 그림을 보며, 현재 상태에서 Linear Gradient 에서 사용하는 값을 유추해보자.

 

1. 시작 지점 

2. 시작 지점의 색상

3. 끝 지점

4. 끝 지점의 색상

 

이 4가지값이 우선적으로 가장 기본이 되는 값이다.

이외에 한가지 더 중요한 값이 있다. 바로 Angle 이다.

 

Lineear Gradient 의 Angle 이다.

위와 같이 Angle 값 -180. 180 사이를 조절하면, Gradient 선이 회전하면서 

색이 선의 기울기에 맞춰 변화하는것을 볼수있다.

 

따라서 정리해보면 Illustrator 를 기준으로 Linear Gradient 를 형성하는데에는 5가지 값이 사용되는것을 알수있다.

 

1. 시작 지점

2. 시작 지점의 색상

3. 끝 지점

4. 끝 지점의 색상

5. 각도 


< Illustrator 에 만든 Linear Gradient 를 어떻게 Juce 코드로 옮겨야하는가? >

 

이제 실제적인 문제에 관해 이야기해보자.

아래와 같이 Illustrator 에서 색을 만들었다고 가정해보자.

 

앞서 Illustrator 에서 우리는 5가지 값을 사용한다고 했었다.

 

1. 시작 지점

2. 시작 지점의 색상

3. 끝 지점

4. 끝 지점의 색상

5. 각도 

 

우린 이것들을 숫자로, 값으로 알아야한다.

일일히 이것을 계산하여 복붙하는것은 조금 괴로운일이다.

Illustrator 내에서 이 값들을 통채로 가져올수있는 방법이 없을까?

방법이 있다. CSS Properties 패널을 사용하는것이다.

이 패널은 내가 현재 선택한 객체를 CSS 코드로 변환해준다.

Juce 코드로 바로 바꾸지는 못하겠지만, 뭐가 있는지 들여다보자.

CSS Properties 패널

linear-gradient(45deg, rgba(228, 226, 77, 1) 0%, rgba(231, 34, 120, 1) 100%);

 

과연 이 값들을 어떻게 사용할수있을지.. Juce 의 생성자를 살펴보자.

ColourGradient (Colour colour1, Point<float> point1, Colour colour2, Point<float> point2, bool isRadial)

 

불행히도 색 이외에는, 요구하는 값이 너무도 틀리다.

 

Illustrator 는 Gradient 의 시작지점과 끝지점을 도형의 가장 왼쪽에서 얼마나 우측으로 이동하였는지 % (퍼센트) 로 표현한다.

가장 왼쪽이 있는 점이면 0%, 가장 우측이면 100% 이다.

그리고나서 Gradient 선의 기울기를 -180. 180 범위의 deg 를 통해 조절한다.

 

반면, ColourGradient 생성자는 각도 조절 인자가 없고,

무조건 시작지점의 x,y 와 끝지점의 x,y 를 Point<float> 객체를 통해 요구한다.

ColourGradient 생정자의 마지막 인자인 bool isRadial 이 false 이면 Linear, Radial 이면 true 를 넘겨야한다.

 

정리를 하면 시작지점과 끝지점을 표현하는 방식이 서로 달라 문제인것이다.

해결책을 찾아야한다.


< Illustrator Gradient 를 Juce Code 로 변환해줄 Helper 함수 만들기 : linearGradient 함수 >

 

우리는 Illustrator 에 수없이 Gradient 색을 만들것이고, 이를 Juce 코드에서 사용하고자할것이다.

위에서 보았듯이, 바로 옮길수없다. 

따라서 바로 옮겨줄수있는 우리만의 함수를 만드는것이 현명하다.

linear-gradient(45deg, rgba(228, 226, 77, 1) 0%, rgba(231, 34, 120, 1) 100%);

위의 인자들을 우선적으로 표현할 함수의 뼈대를 만들어보자.

juce::ColourGradient linearGradient (

    float                   degree,         // -180. ~ 180.
    juce::Colour            startColour,
    float                   startPerecent,  // 0. ~ 100.
    juce::Colour            endColour,
    float                   endPercent,     // 0. ~ 100.
    juce::Rectangle<int>    bounds

)

위의 코드에는, 5번째 인자로 bounds 가 추가 되었다.

코드에서 상대적인 startPercent 와 endPercent 를 

Juce 의 ColourGradient 생성자의 인자인 절대적인 위치 x, y 를 계산할때 Component 의 크기를 알아야하기때문이다.

 

우리의 linearGradient 의 함수 내부에서는 이 생성자를 사용할것이다.

ColourGradient (Colour colour1, float x1, float y1, Colour colour2, float x2, float y2, bool isRadial)

 

이제 계산해보자.

 

"이제 계산해보자" 를 적은뒤 4시간이 흘러 코드를 완성하였다.....

왜냐면 꽤나 귀찮은 수학이 필요하기때문이다.

Stackoverflow 를 미친듯이 뒤진끝에 JavaScript 지만 적절한 코드를 발견하였다.

 

Get new x,y coordinates of a point in a rotated image

function rotate(x, y, xm, ym, a) {
    var cos = Math.cos,
        sin = Math.sin,

        a = a * Math.PI / 180, // Convert to radians because that is what
                               // JavaScript likes

        // Subtract midpoints, so that midpoint is translated to origin
        // and add it in the end again
        xr = (x - xm) * cos(a) - (y - ym) * sin(a)   + xm,
        yr = (x - xm) * sin(a) + (y - ym) * cos(a)   + ym;

    return [xr, yr];
}

rotate(16, 32, 16, 16, 30); // [8, 29.856...]

 

위의 방식을 적용하여,

Illustrator 에서 추출한 CSS 코드를 인자로 넘기는 linearGradient() 함수를 만들었다.

juce::ColourGradient linearGradient (

    float                   degree,         // -180. ~ 180.
    juce::Colour            startColour,
    float                   startPerecent,  // 0. ~ 100.
    juce::Colour            endColour,
    float                   endPercent,     // 0. ~ 100.
    juce::Rectangle<float>  bounds

) {
    
    // Convert start & end percent to x coordinate.
    const auto width    = bounds.getWidth();
    
    const auto xStart   = (startPerecent / 100.0f) * width;
    const auto xEnd     = (endPercent    / 100.0f) * width;

    // Caculate x1, y2, x2, y2 as rotating xStart & xEnd.
    // We need to multiply -1.0f to degree, because Adobe Illustrator rotates gradient opposite.
    const auto radian   = degree * (juce::MathConstants<float>::pi / 180.0f) * -1.0f;
    const auto xCenter  = bounds.getCentreX();
    const auto yCetner  = bounds.getCentreY();
    
    const auto x1 = ((xStart - xCenter)  * std::cos(radian)) - std::sin(radian) + xCenter;
    const auto y1 = ((xStart - xCenter)  * std::sin(radian)) + std::cos(radian) + yCetner;
    const auto x2 = ((xEnd - xCenter)    * std::cos(radian)) - std::sin(radian) + xCenter;
    const auto y2 = ((xEnd - xCenter)    * std::sin(radian)) + std::cos(radian) + yCetner;
    
    // Return ColourGradient object.
    return { startColour, x1, y1, endColour, x2, y2, false };

}

 

코드에 대해 한가지만 설명하면

radian 에 -1.0f 를 곱했는데 이유는 방향을 반대로 돌려야, Illustrator 과 결과물과 똑같이 된다.

위의 코드를 활용하여 더 간소한 startPercent 와 endPercent 를 따로 넘길 필요없는 

동일한 linearGradient 이름을 가진 간소한 형태의 함수를 하나더 오버로딩하였다.

juce::ColourGradient linearGradient (
    
    float degree,
    juce::Colour startColour,
    juce::Colour endColour,
    juce::Rectangle<float> bounds
    
) {

    return linearGradient(degree, startColour, 0.0f, endColour, 100.0f, bounds);

}

 

어쨋든 이제 코드 내부가 어찌되는지는 잊고, 사용해보자...

 

paint() 안에서 아래와 같이 호출하면..

void paint(juce::Graphics& g) override {

  // from Illustrator : 
  // linear-gradient(45deg, rgba(227, 225, 77, 1) 0%, rgba(230, 33, 120, 1) 100%);
  auto gradient = linearGradient(60.0f, {227, 225, 77}, 0.0f, {230, 33, 120}, 100.0f, getLocalBounds().toFloat());

  g.setGradientFill(gradient);
  g.fillAll();


}

 

짜잔!

좌측 : 일러스트레이터 / 우측 : Juce

간절히 원하면... 이루어지는것같다.

 

완성본.

juce::ColourGradient linearGradient (

    float                   degree,         // -180. ~ 180.
    juce::Colour            startColour,
    float                   startPerecent,  // 0. ~ 100.
    juce::Colour            endColour,
    float                   endPercent,     // 0. ~ 100.
    juce::Rectangle<float>  bounds

) {
    
    // Convert start & end percent to x coordinate.
    const auto width    = bounds.getWidth();
    
    const auto xStart   = (startPerecent / 100.0f) * width;
    const auto xEnd     = (endPercent    / 100.0f) * width;

    // Caculate x1, y2, x2, y2 as rotating xStart & xEnd.
    // We need to multiply -1.0f to degree, because Adobe Illustrator rotates gradient opposite.
    const auto radian   = degree * (juce::MathConstants<float>::pi / 180.0f) * -1.0f;
    const auto xCenter  = bounds.getCentreX();
    const auto yCetner  = bounds.getCentreY();
    
    const auto x1 = ((xStart - xCenter)  * std::cos(radian)) - std::sin(radian) + xCenter;
    const auto y1 = ((xStart - xCenter)  * std::sin(radian)) + std::cos(radian) + yCetner;
    const auto x2 = ((xEnd - xCenter)    * std::cos(radian)) - std::sin(radian) + xCenter;
    const auto y2 = ((xEnd - xCenter)    * std::sin(radian)) + std::cos(radian) + yCetner;
    
    // Return ColourGradient object.
    return { startColour, x1, y1, endColour, x2, y2, false };

}

juce::ColourGradient linearGradient (
    
    float degree,
    juce::Colour startColour,
    juce::Colour endColour,
    juce::Rectangle<float> bounds
    
) {

    return linearGradient(degree, startColour, 0.0f, endColour, 100.0f, bounds);

}


void paint(juce::Graphics& g) override {

    auto gradient = linearGradient(0.0f, {227, 225, 77}, {230, 33, 120}, getLocalBounds().toFloat());
    
    g.setGradientFill(gradient);
    g.fillAll();
    
    
}

 

추가 1)

 

Peter 에 말에 의하면 사용자 정의 리터럴을 사용하면 좋을것같다고했다.

그럼 인자로 60deg 를 전달하기위해 60.0f 를 넣는대신 실제로 60deg 를 넘길수있고,

코드 내부에서 percent 를 0. ~ 1. 범위로 만들기위해 100.0f 로 나누고있는데, 이것도 인자로 100% 로 넘길수있다고한다.

 

추가 2 : Point 클래스의 getPointOnCircumference() 함수 )

 

Rectangle 과 Point 클래스의 내부 함수들을 활용하여 리펙토링할수있거같다고 Peter 가 말했다.

위의 나의 코드와 같이 복잡한 계산을 덜 할수있도록, 

Point 클래스에 아래와 같은 함수가 존재한다고 한다.

 

Point.클래스의 getPointOnCircumference

또한 Rectangle 클래스의 getCenter : Point 객체를 return 한다.

이를 활용해 Peter 가 보내준 짧은 코드.

auto rect = bounds.toFloat();
auto center = rect.getCentre();
auto radius = std::max(rect.getWidth(), rect.getHeight());
auto p1 = center.getPointOnCircumference(radius, angle);
auto p2 = center.getPointOnCircumference(radius, angle + juce::MathConstants<float>::pi);
p1 = rect.getConstrainedPoint(p1);
p2 = rect.getConstrainedPoint(p2)

각 x, y 지점을 따로 float 변수로 계산하는대신, Point 와 Rectangle 객체를 주고받으며 활용해보라는 이야기인듯하다.

추후 다시 리팩토링을 위의 방식으로 다시 해보기로한다.


2. Radial Gradient (원형 그라디언트)

 


 

 

< 디자인적 결정은 디자인툴에서... : Gradient 값은 Illustrator 가 알고있다! >

 

앞서 Colour 클래스에서 누누히 이야기했듯이.. 우리는 이 절대적인 원칙을 잊지말아야한다.

"디자인적 결정은 디자인툴에서"

Juce 에서 코드로 지지고볶고하며, 적절한 Gradient 색을 만들겠다는 바보같은 생각은 절대로 하지말아야한다.

VST 개발은 절대 디자인이 전부가 아니며.. GUI 쪽이 그나마 디자인툴에 의지할수있는 유일한 세상이다..

이에 감사하며, Illustrator 를 적극적으로 활용하기를 바란다. 

굳이 Gradient 가 아니어도, 추후 우리를 개고생시킬 VST 개발의 다른 개같은 난이도의 영역들이 우리를 기다리고있기때문이다.

(이때 조금 이나마 기댈수있는곳은 Cycling'74 의 Max 이다)

 


< 우리가 만든 Gradient 를 Juce 코드로 .. >

 


< ColourGradient 를 사용하는 유일한 함수 : Graphics 의 setGradientFill >

void setGradientFill (const ColourGradient& gradient)