"""Utilities for working with gradients. Inspired by Compass, but not quite
the same.
"""
from __future__ import absolute_import
import base64
import logging
import six
from scss.functions.library import FunctionLibrary
from scss.functions.compass.helpers import opposite_position, position
from scss.types import ColorValue, List, NumberValue, StringValue
from scss.util import escape, split_params, to_float, to_str
log = logging.getLogger(__name__)
COMPASS_GRADIENTS_LIBRARY = FunctionLibrary()
register = COMPASS_GRADIENTS_LIBRARY.register
# ------------------------------------------------------------------------------
def __color_stops(percentages, *args):
if len(args) == 1:
if isinstance(args[0], (list, tuple, List)):
list(args[0])
elif isinstance(args[0], (StringValue, six.string_types)):
color_stops = []
colors = split_params(getattr(args[0], 'value', args[0]))
for color in colors:
color = color.strip()
if color.startswith('color-stop('):
s, c = split_params(color[11:].rstrip(')'))
s = s.strip()
c = c.strip()
else:
c, s = color.split()
color_stops.append((to_float(s), c))
return color_stops
colors = []
stops = []
prev_color = False
for c in args:
for c in List.from_maybe(c):
if isinstance(c, ColorValue):
if prev_color:
stops.append(None)
colors.append(c)
prev_color = True
elif isinstance(c, NumberValue):
stops.append(c)
prev_color = False
if prev_color:
stops.append(None)
stops = stops[:len(colors)]
if stops[0] is None:
stops[0] = NumberValue(0, '%')
if stops[-1] is None:
stops[-1] = NumberValue(100, '%')
if percentages:
max_stops = max(s and (s.value if s.unit != '%' else None) or None for s in stops)
else:
max_stops = max(s if s and s.unit != '%' else None for s in stops)
stops = [s / max_stops if s and s.unit != '%' else s for s in stops]
init = 0
start = None
for i, s in enumerate(stops + [1.0]):
if s is None:
if start is None:
start = i
end = i
else:
final = s
if start is not None:
stride = (final - init) / NumberValue(end - start + 1 + (1 if i < len(stops) else 0))
for j in range(start, end + 1):
stops[j] = init + stride * NumberValue(j - start + 1)
init = final
start = None
if not max_stops or percentages:
pass
else:
stops = [s * max_stops for s in stops]
return zip(stops, colors)
@register('grad-color-stops')
def grad_color_stops(*args):
args = List.from_maybe_starargs(args)
color_stops = __color_stops(True, *args)
ret = ', '.join(['color-stop(%s, %s)' % (to_str(s), c) for s, c in color_stops])
return StringValue(ret)
def __grad_end_position(radial, color_stops):
return __grad_position(-1, 100, radial, color_stops)
@register('grad-point')
def grad_point(*p):
pos = set()
hrz = vrt = NumberValue(0.5, '%')
for _p in p:
pos.update(StringValue(_p).value.split())
if 'left' in pos:
hrz = NumberValue(0, '%')
elif 'right' in pos:
hrz = NumberValue(1, '%')
if 'top' in pos:
vrt = NumberValue(0, '%')
elif 'bottom' in pos:
vrt = NumberValue(1, '%')
return List([v for v in (hrz, vrt) if v is not None])
def __grad_position(index, default, radial, color_stops):
try:
stops = NumberValue(color_stops[index][0])
if radial and stops.unit != 'px' and (index == 0 or index == -1 or index == len(color_stops) - 1):
log.warn("Webkit only supports pixels for the start and end stops for radial gradients. Got %s", stops)
except IndexError:
stops = NumberValue(default)
return stops
@register('grad-end-position')
def grad_end_position(*color_stops):
color_stops = __color_stops(False, *color_stops)
return NumberValue(__grad_end_position(False, color_stops))
@register('color-stops')
def color_stops(*args):
args = List.from_maybe_starargs(args)
color_stops = __color_stops(False, *args)
ret = ', '.join(['%s %s' % (c, to_str(s)) for s, c in color_stops])
return StringValue(ret)
@register('color-stops-in-percentages')
def color_stops_in_percentages(*args):
args = List.from_maybe_starargs(args)
color_stops = __color_stops(True, *args)
ret = ', '.join(['%s %s' % (c, to_str(s)) for s, c in color_stops])
return StringValue(ret)
def _get_gradient_position_and_angle(args):
for arg in args:
if isinstance(arg, (StringValue, NumberValue, six.string_types)):
_arg = [arg]
elif isinstance(arg, (list, tuple, List)):
_arg = arg
else:
continue
ret = None
skip = False
for a in _arg:
if isinstance(a, ColorValue):
skip = True
break
elif isinstance(a, NumberValue):
ret = arg
if skip:
continue
if ret is not None:
return ret
for seek in (
'center',
'top', 'bottom',
'left', 'right',
):
if seek in _arg:
return arg
return None
def _get_gradient_shape_and_size(args):
for arg in args:
if isinstance(arg, (StringValue, NumberValue, six.string_types)):
_arg = [arg]
elif isinstance(arg, (list, tuple, List)):
_arg = arg
else:
continue
for seek in (
'circle', 'ellipse',
'closest-side', 'closest-corner',
'farthest-side', 'farthest-corner',
'contain', 'cover',
):
if seek in _arg:
return arg
return None
def _get_gradient_color_stops(args):
color_stops = []
for arg in args:
for a in List.from_maybe(arg):
if isinstance(a, ColorValue):
color_stops.append(arg)
break
return color_stops or None
@register('radial-gradient')
def radial_gradient(*args):
args = List.from_maybe_starargs(args)
position_and_angle = _get_gradient_position_and_angle(args)
shape_and_size = _get_gradient_shape_and_size(args)
color_stops = _get_gradient_color_stops(args)
if color_stops is None:
raise Exception('No color stops provided to radial-gradient function')
color_stops = __color_stops(False, *color_stops)
args = [
position(position_and_angle) if position_and_angle is not None else None,
shape_and_size if shape_and_size is not None else None,
]
args.extend('%s %s' % (c, to_str(s)) for s, c in color_stops)
to__s = 'radial-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
ret = StringValue(to__s)
def to__css2():
return StringValue('')
ret.to__css2 = to__css2
def to__moz():
return StringValue('-moz-' + to__s)
ret.to__moz = to__moz
def to__pie():
log.warn("PIE does not support radial-gradient.")
return StringValue('-pie-radial-gradient(unsupported)')
ret.to__pie = to__pie
def to__webkit():
return StringValue('-webkit-' + to__s)
ret.to__webkit = to__webkit
def to__owg():
args = [
'radial',
grad_point(position_and_angle) if position_and_angle is not None else 'center',
'0',
grad_point(position_and_angle) if position_and_angle is not None else 'center',
__grad_end_position(True, color_stops),
]
args.extend('color-stop(%s, %s)' % (to_str(s), c) for s, c in color_stops)
ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
return StringValue(ret)
ret.to__owg = to__owg
def to__svg():
return radial_svg_gradient(color_stops, position_and_angle or 'center')
ret.to__svg = to__svg
return ret
@register('linear-gradient')
def linear_gradient(*args):
args = List.from_maybe_starargs(args)
position_and_angle = _get_gradient_position_and_angle(args)
color_stops = _get_gradient_color_stops(args)
if color_stops is None:
raise Exception('No color stops provided to linear-gradient function')
color_stops = __color_stops(False, *color_stops)
args = [
position(position_and_angle) if position_and_angle is not None else None,
]
args.extend('%s %s' % (c, to_str(s)) for s, c in color_stops)
to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
ret = StringValue(to__s, quotes=None)
def to__css2():
return StringValue('', quotes=None)
ret.to__css2 = to__css2
def to__moz():
return StringValue('-moz-' + to__s, quotes=None)
ret.to__moz = to__moz
def to__pie():
return StringValue('-pie-' + to__s, quotes=None)
ret.to__pie = to__pie
def to__ms():
return StringValue('-ms-' + to__s, quotes=None)
ret.to__ms = to__ms
def to__o():
return StringValue('-o-' + to__s, quotes=None)
ret.to__o = to__o
def to__webkit():
return StringValue('-webkit-' + to__s, quotes=None)
ret.to__webkit = to__webkit
def to__owg():
args = [
'linear',
position(position_and_angle or None),
opposite_position(position_and_angle or None),
]
args.extend('color-stop(%s, %s)' % (to_str(s), c) for s, c in color_stops)
ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
return StringValue(ret, quotes=None)
ret.to__owg = to__owg
def to__svg():
return linear_svg_gradient(color_stops, position_and_angle or 'top')
ret.to__svg = to__svg
return ret
@register('radial-svg-gradient')
def radial_svg_gradient(*args):
args = List.from_maybe_starargs(args)
color_stops = args
center = None
if isinstance(args[-1], (StringValue, NumberValue, six.string_types)):
center = args[-1]
color_stops = args[:-1]
color_stops = __color_stops(False, *color_stops)
cx, cy = zip(*grad_point(center).items())[1]
r = __grad_end_position(True, color_stops)
svg = __radial_svg(color_stops, cx, cy, r)
url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg)
inline = 'url("%s")' % escape(url)
return StringValue(inline)
@register('linear-svg-gradient')
def linear_svg_gradient(*args):
args = List.from_maybe_starargs(args)
color_stops = args
start = None
if isinstance(args[-1], (StringValue, NumberValue, six.string_types)):
start = args[-1]
color_stops = args[:-1]
color_stops = __color_stops(False, *color_stops)
x1, y1 = zip(*grad_point(start).items())[1]
x2, y2 = zip(*grad_point(opposite_position(start)).items())[1]
svg = _linear_svg(color_stops, x1, y1, x2, y2)
url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg)
inline = 'url("%s")' % escape(url)
return StringValue(inline)
def __color_stops_svg(color_stops):
ret = ''.join('' % (to_str(s), c) for s, c in color_stops)
return ret
def __svg_template(gradient):
ret = '\
' % gradient
return ret
def _linear_svg(color_stops, x1, y1, x2, y2):
gradient = '%s' % (
to_str(NumberValue(x1)),
to_str(NumberValue(y1)),
to_str(NumberValue(x2)),
to_str(NumberValue(y2)),
__color_stops_svg(color_stops)
)
return __svg_template(gradient)
def __radial_svg(color_stops, cx, cy, r):
gradient = '%s' % (
to_str(NumberValue(cx)),
to_str(NumberValue(cy)),
to_str(NumberValue(r)),
__color_stops_svg(color_stops)
)
return __svg_template(gradient)