Skip to content

Commit

Permalink
Gradients Node (#1544)
Browse files Browse the repository at this point in the history
* gradient node

- 1, 3, 4 channels (color picker widget would be nice)
- horizontal, vertical, diagonal, radial, conic
- up to three color levels per gradient (UI is awkward)

* remove pattern node (not ready yet)

* parameterize the circular gradients
set channels in output type

* black, lint

* ...

* Change node to just output greyscale.

- More modular
- Can use a LUT to map colors if needed
- Punts on the need for a color picker widget
- NOTE: did this on my laptop without testing, so this might not actually run

* ...

* respond to feedback

- add "reverse" checkbox
- "angle" and "width" controls for diagonal
  • Loading branch information
adodge authored Feb 14, 2023
1 parent 1c3b594 commit d1b275d
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 0 deletions.
59 changes: 59 additions & 0 deletions backend/src/nodes/impl/gradients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import math
import numpy as np


def horizontal_gradient(img: np.ndarray):
x = np.arange(img.shape[1])
p = x / (img.shape[1] - 1)
img[:, :] = p.reshape((1, -1))


def vertical_gradient(img: np.ndarray):
x = np.arange(img.shape[0])
p = x / (img.shape[0] - 1)
img[:, :] = p.reshape((-1, 1))


def diagonal_gradient(img: np.ndarray, angle: float, width: float):
center = np.array([img.shape[0], img.shape[1]], dtype=np.float32) / 2
direction = np.array([np.cos(angle), np.sin(angle)], dtype=np.float32)

start = center - direction * width / 2

pixels = np.array(
[[(r, c) for r in range(img.shape[0]) for c in range(img.shape[1])]]
)
projection = (pixels - start).dot(direction)
p = np.clip((projection / width).ravel(), 0, 1)
img[:] = p.reshape(img.shape)


def radial_gradient(
img: np.ndarray, inner_radius_percent: float = 0, outer_radius_percent: float = 1
):
inner_radius = inner_radius_percent * img.shape[1] / 2
outer_radius = outer_radius_percent * img.shape[1] / 2

center = np.array(img.shape[:2], dtype="float32") / 2
pixels = np.array(
[(r, c) for r in range(img.shape[0]) for c in range(img.shape[1])]
)
distance = np.sqrt(np.sum((pixels - center) ** 2, axis=1))
p = (distance - inner_radius) / (outer_radius - inner_radius)
img[:] = p.reshape(img.shape)


def conic_gradient(img: np.ndarray, rotation: float = 0):
if rotation > np.pi:
rotation -= 2 * np.pi
if rotation < -np.pi:
rotation += 2 * np.pi

center = np.array(img.shape[:2], dtype="float32") / 2
pixels = np.array(
[(r, c) for r in range(img.shape[0]) for c in range(img.shape[1])]
)
angles = np.arctan2(pixels[:, 0] - center[0], pixels[:, 1] - center[1]) + rotation
angles[angles < 0] += 2 * np.pi
p = angles / math.pi / 2
img[:] = p.reshape(img.shape)
144 changes: 144 additions & 0 deletions backend/src/nodes/nodes/image_utility/create_gradient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from __future__ import annotations

from enum import Enum

import numpy as np

from . import category as ImageUtilityCategory
from ...impl.gradients import (
horizontal_gradient,
vertical_gradient,
diagonal_gradient,
radial_gradient,
conic_gradient,
)
from ...node_base import NodeBase, group
from ...node_factory import NodeFactory
from ...properties import expression
from ...properties.inputs import (
NumberInput,
EnumInput,
SliderInput,
BoolInput,
)
from ...properties.outputs import ImageOutput


class GradientStyle(Enum):
HORIZONTAL = "Horizontal"
VERTICAL = "Vertical"
DIAGONAL = "Diagonal"
RADIAL = "Radial"
CONIC = "Conic"


@NodeFactory.register("chainner:image:create_gradient")
class CreateGradientNode(NodeBase):
def __init__(self):
super().__init__()
self.description = "Create an image with a gradient."
self.inputs = [
NumberInput("Width", minimum=1, unit="px", default=64),
NumberInput("Height", minimum=1, unit="px", default=64),
BoolInput("Reverse", default=False),
EnumInput(GradientStyle, default_value=GradientStyle.HORIZONTAL).with_id(3),
group(
"conditional-enum",
{
"enum": 3,
"conditions": [
[GradientStyle.DIAGONAL.value],
[GradientStyle.DIAGONAL.value],
[GradientStyle.RADIAL.value],
[GradientStyle.RADIAL.value],
[GradientStyle.CONIC.value],
],
},
)(
SliderInput(
"Angle",
minimum=0,
maximum=360,
default=45,
unit="deg",
),
NumberInput(
"Width",
minimum=0,
default=100,
unit="px",
),
SliderInput(
"Inner Radius",
minimum=0,
maximum=100,
default=0,
unit="%",
),
SliderInput(
"Outer Radius",
minimum=0,
maximum=100,
default=100,
unit="%",
),
SliderInput(
"Rotation",
minimum=0,
maximum=360,
default=0,
unit="deg",
),
),
]
self.outputs = [
ImageOutput(
image_type=expression.Image(
width="Input0",
height="Input1",
channels=1,
)
)
]
self.category = ImageUtilityCategory
self.name = "Create Gradient"
self.icon = "MdFormatColorFill"
self.sub = "Create Images"

def run(
self,
width: int,
height: int,
reverse: bool,
gradient_style: GradientStyle,
diagonal_angle: float,
diagonal_width: float,
inner_radius_percent: float,
outer_radius_percent: float,
conic_rotation: float,
) -> np.ndarray:
img = np.zeros((height, width), dtype=np.float32)

if gradient_style == GradientStyle.HORIZONTAL:
horizontal_gradient(img)

elif gradient_style == GradientStyle.VERTICAL:
vertical_gradient(img)

elif gradient_style == GradientStyle.DIAGONAL:
diagonal_gradient(img, diagonal_angle * np.pi / 180, diagonal_width)

elif gradient_style == GradientStyle.RADIAL:
radial_gradient(
img,
inner_radius_percent=inner_radius_percent / 100,
outer_radius_percent=outer_radius_percent / 100,
)

elif gradient_style == GradientStyle.CONIC:
conic_gradient(img, rotation=conic_rotation * np.pi / 180)

if reverse:
img = 1 - img

return img

0 comments on commit d1b275d

Please sign in to comment.