"""Utilities for working with gradients. Inspired by Compass, but not quite the same. """ from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals import base64 import logging import six from . import CompassExtension from .helpers import opposite_position, position from scss.types import Color, List, Number, String from scss.util import escape, split_params, to_float, to_str log = logging.getLogger(__name__) ns = CompassExtension.namespace def _is_color(value): # currentColor is not a Sass color value, but /is/ a CSS color value return isinstance(value, Color) or value == String('currentColor') def __color_stops(percentages, *args): if len(args) == 1: if isinstance(args[0], (list, tuple, List)): return list(args[0]) elif isinstance(args[0], (String, 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 _is_color(c): if prev_color: stops.append(None) colors.append(c) prev_color = True elif isinstance(c, Number): stops.append(c) prev_color = False if prev_color: stops.append(None) stops = stops[:len(colors)] if stops[0] is None or stops[0] == Number(0): stops[0] = Number(0, '%') if stops[-1] is None: stops[-1] = Number(100, '%') maxable_stops = [s for s in stops if s and not s.is_simple_unit('%')] if maxable_stops: max_stops = max(maxable_stops) else: max_stops = None stops = [_s / max_stops if _s and not _s.is_simple_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) / Number(end - start + 1 + (1 if i < len(stops) else 0)) for j in range(start, end + 1): stops[j] = init + stride * Number(j - start + 1) init = final start = None if not max_stops or percentages: pass else: stops = [s if s.is_simple_unit('%') else s * max_stops for s in stops] return List(List(pair) for pair in zip(stops, colors)) def _render_standard_color_stops(color_stops): pairs = [] for i, (stop, color) in enumerate(color_stops): if ((i == 0 and stop == Number(0, '%')) or (i == len(color_stops) - 1 and stop == Number(100, '%'))): pairs.append(color) else: pairs.append(List([color, stop], use_comma=False)) return List(pairs, use_comma=True) @ns.declare def grad_color_stops(*args): args = List.from_maybe_starargs(args) color_stops = __color_stops(True, *args) ret = ', '.join(['color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops]) return String.unquoted(ret) def __grad_end_position(radial, color_stops): return __grad_position(-1, 100, radial, color_stops) @ns.declare def grad_point(*p): pos = set() hrz = vrt = Number(0.5, '%') for _p in p: pos.update(String.unquoted(_p).value.split()) if 'left' in pos: hrz = Number(0, '%') elif 'right' in pos: hrz = Number(1, '%') if 'top' in pos: vrt = Number(0, '%') elif 'bottom' in pos: vrt = Number(1, '%') return List([v for v in (hrz, vrt) if v is not None]) def __grad_position(index, default, radial, color_stops): try: stops = Number(color_stops[index][0]) if radial and not stops.is_simple_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 = Number(default) return stops @ns.declare def grad_end_position(*color_stops): color_stops = __color_stops(False, *color_stops) return Number(__grad_end_position(False, color_stops)) @ns.declare def color_stops(*args): args = List.from_maybe_starargs(args) color_stops = __color_stops(False, *args) ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) return String.unquoted(ret) @ns.declare def color_stops_in_percentages(*args): args = List.from_maybe_starargs(args) color_stops = __color_stops(True, *args) ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) return String.unquoted(ret) def _get_gradient_position_and_angle(args): for arg in args: ret = None skip = False for a in arg: if _is_color(a): skip = True break elif isinstance(a, Number): ret = arg if skip: continue if ret is not None: return ret for seek in ( 'center', 'top', 'bottom', 'left', 'right', ): if String(seek) in arg: return arg return None def _get_gradient_shape_and_size(args): for arg in args: for seek in ( 'circle', 'ellipse', 'closest-side', 'closest-corner', 'farthest-side', 'farthest-corner', 'contain', 'cover', ): if String(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 _is_color(a): color_stops.append(arg) break return color_stops or None # TODO these functions need to be # 1. well-defined # 2. guaranteed to never wreck css3 syntax # 3. updated to whatever current compass does # 4. fixed to use a custom type instead of monkeypatching @ns.declare def radial_gradient(*args): args = List.from_maybe_starargs(args) try: # Do a rough check for standard syntax first -- `shape at position` at_position = list(args[0]).index(String('at')) except (IndexError, ValueError): shape_and_size = _get_gradient_shape_and_size(args) position_and_angle = _get_gradient_position_and_angle(args) else: shape_and_size = List.maybe_new(args[0][:at_position]) position_and_angle = List.maybe_new(args[0][at_position + 1:]) 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) if position_and_angle: rendered_position = position(position_and_angle) else: rendered_position = None rendered_color_stops = _render_standard_color_stops(color_stops) args = [] if shape_and_size and rendered_position: args.append(List([shape_and_size, String.unquoted('at'), rendered_position], use_comma=False)) elif rendered_position: args.append(rendered_position) elif shape_and_size: args.append(shape_and_size) args.extend(rendered_color_stops) legacy_args = [] if rendered_position: legacy_args.append(rendered_position) if shape_and_size: legacy_args.append(shape_and_size) legacy_args.extend(rendered_color_stops) ret = String.unquoted( 'radial-gradient(' + ', '.join(a.render() for a in args) + ')') legacy_ret = 'radial-gradient(' + ', '.join(a.render() for a in legacy_args) + ')' def to__css2(): return String.unquoted('') ret.to__css2 = to__css2 def to__moz(): return String.unquoted('-moz-' + legacy_ret) ret.to__moz = to__moz def to__pie(): log.warn("PIE does not support radial-gradient.") return String.unquoted('-pie-radial-gradient(unsupported)') ret.to__pie = to__pie def to__webkit(): return String.unquoted('-webkit-' + legacy_ret) 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)' % (s.render(), c.render()) for s, c in color_stops) ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' return String.unquoted(ret) ret.to__owg = to__owg def to__svg(): return radial_svg_gradient(*(list(color_stops) + list(position_and_angle or [String('center')]))) ret.to__svg = to__svg return ret @ns.declare 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(_render_standard_color_stops(color_stops)) to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' ret = String.unquoted(to__s) def to__css2(): return String.unquoted('') ret.to__css2 = to__css2 def to__moz(): return String.unquoted('-moz-' + to__s) ret.to__moz = to__moz def to__pie(): return String.unquoted('-pie-' + to__s) ret.to__pie = to__pie def to__ms(): return String.unquoted('-ms-' + to__s) ret.to__ms = to__ms def to__o(): return String.unquoted('-o-' + to__s) ret.to__o = to__o def to__webkit(): return String.unquoted('-webkit-' + to__s) 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)' % (s.render(), c.render()) for s, c in color_stops) ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args if a is not None) + ')' return String.unquoted(ret) 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 @ns.declare def radial_svg_gradient(*args): args = List.from_maybe_starargs(args) color_stops = args center = None if isinstance(args[-1], (String, Number)): center = args[-1] color_stops = args[:-1] color_stops = __color_stops(False, *color_stops) cx, cy = grad_point(center) 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 String.unquoted(inline) @ns.declare def linear_svg_gradient(*args): args = List.from_maybe_starargs(args) color_stops = args start = None if isinstance(args[-1], (String, Number)): start = args[-1] color_stops = args[:-1] color_stops = __color_stops(False, *color_stops) x1, y1 = grad_point(start) x2, y2 = grad_point(opposite_position(start)) svg = _linear_svg(color_stops, x1, y1, x2, y2) url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) inline = 'url("%s")' % escape(url) return String.unquoted(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 = '\ \ %s\ \ ' % gradient return ret def _linear_svg(color_stops, x1, y1, x2, y2): gradient = '%s' % ( to_str(Number(x1)), to_str(Number(y1)), to_str(Number(x2)), to_str(Number(y2)), __color_stops_svg(color_stops) ) return __svg_template(gradient) def __radial_svg(color_stops, cx, cy, r): gradient = '%s' % ( to_str(Number(cx)), to_str(Number(cy)), to_str(Number(r)), __color_stops_svg(color_stops) ) return __svg_template(gradient)