Source code for vis_tools.Gradient
# ==============================================================================
# Gradient.py - A color gradient class
# ==============================================================================
"""Gradient.py
This file contains a class for doing Python-side color gradients. The
Gradient class is a gradient with an arbitrary number of color stops. Since
Gradient is derived from the Vis-Tools gradient.js, the same style text
representations of gradients can be used.
Classes:
* Gradient - a sampleable color gradient.
Usage::
gradient = Gradient("green@0,[email protected],[email protected],red@1")
color = gradient.sample(0.5)
"""
# imports
from builtins import str
from builtins import object
from vis_tools import Color, NamedColors
# ==============================================================================
# Gradient - a simple color gradient class
# ==============================================================================
[docs]class Gradient(object):
"""Class for sampleable color gradients.
This class lets you create a color gradient with an arbitrary number of
color stops on a normalized range from 0 to 1. You then sample the gradient
with a normalized value from 0 to 1 to get a color out of the gradient.
This class is based on the Vis-Tools gradient.js. However it does not
support the ",r" or ",q<steps>" suffixes supported by gradient.js.
The spec format is:
<color>@0,[<color>@<loc>,...]<color>@1
Where:
* color - Color objects representing the stop colors
* loc - Ordered values in range(0, 1) representing the normalized
locations of the gradient stops.
Raises:
ValueError: if spec is invalid.
"""
def __init__(self, spec=""):
"""Construct a Gradient from a text specification.
Args:
spec (str): See the class section above for details.
"""
self.stops = [
{"color": Color(0, 0, 0), "location": 0.0},
{"color": Color(255, 255, 255), "location": 1.0}
]
if len(spec) > 0:
stops = self._parse_spec(spec)
if len(stops) > 0:
self.stops = stops
# --------------------------------------------------------------------------
def __str__(self):
"""Generates a textual representation of a Gradient.
The returned string is sufficient for recreating the Gradient via the
constructor.
Returns:
str: String representation of gradient.
Args:
None.
"""
result = ""
stop_index = 0
for stop in self.stops:
if stop_index > 0:
result += ","
# use str() here, not repr()
result += str(stop["color"]) + "@" + str(stop["location"])
stop_index += 1
return result
# --------------------------------------------------------------------------
[docs] def sample(self, loc0to1):
"""Sample the gradient to get a color at a particular location.
Returns:
obj: A Color object for the color at the sample point.
Args:
loc0to1 (float): A normalized value in the range(0, 1) at which
point to sample the gradient color.
"""
loc = Gradient._clamp(loc0to1, 0.0, 1.0)
high_index = 0
for stop in self.stops:
if stop["location"] > loc:
break
high_index += 1
if high_index <= 0 or high_index >= len(self.stops):
high_index = len(self.stops) - 1
high_stop = self.stops[high_index]
low_stop = self.stops[high_index - 1]
rng = high_stop["location"] - low_stop["location"]
sub_loc = (loc - low_stop["location"]) / rng
low = low_stop["color"].r
high = high_stop["color"].r
red = low + sub_loc * (high - low)
low = low_stop["color"].g
high = high_stop["color"].g
green = low + sub_loc * (high - low)
low = low_stop["color"].b
high = high_stop["color"].b
blue = low + sub_loc * (high - low)
return Color(int(red), int(green), int(blue))
# --------------------------------------------------------------------------
# Implementation
# --------------------------------------------------------------------------
@staticmethod
def _clamp(x, min_inclusive, max_inclusive):
if x < min_inclusive:
x = min_inclusive
if x > max_inclusive:
x = max_inclusive
return x
# --------------------------------------------------------------------------
@staticmethod
def _parse_spec(spec):
stops = []
parts = spec.split(",")
if len(parts) < 2:
raise ValueError("Minimum two stops required.")
for part in parts:
part = part.strip()
subparts = part.split("@")
if len(subparts) != 2:
raise ValueError("Stops require a color and location.")
if subparts[0][0] == '#':
# #rrggbb color
r = int(subparts[0][1:3], 16)
g = int(subparts[0][3:5], 16)
b = int(subparts[0][5:7], 16)
color = Color(r, g, b)
elif subparts[0] in NamedColors.__dict__:
# named color
color = NamedColors.__dict__[subparts[0]]
else:
color = Color(0, 0, 0)
location = float(subparts[1])
if location < 0.0 or location > 1.0:
raise ValueError("Location out of range.")
stop = {"color": color, "location": location}
stops.append(stop)
return stops