1"""Utilities for working with gradients. Inspired by Compass, but not quite 2the same. 3""" 4from __future__ import absolute_import 5from __future__ import print_function 6from __future__ import unicode_literals 7 8import base64 9import logging 10 11import six 12 13from . import CompassExtension 14from .helpers import opposite_position, position 15from scss.types import Color, List, Number, String 16from scss.util import escape, split_params, to_float, to_str 17 18log = logging.getLogger(__name__) 19ns = CompassExtension.namespace 20 21 22def _is_color(value): 23 # currentColor is not a Sass color value, but /is/ a CSS color value 24 return isinstance(value, Color) or value == String('currentColor') 25 26 27def __color_stops(percentages, *args): 28 if len(args) == 1: 29 if isinstance(args[0], (list, tuple, List)): 30 return list(args[0]) 31 elif isinstance(args[0], (String, six.string_types)): 32 color_stops = [] 33 colors = split_params(getattr(args[0], 'value', args[0])) 34 for color in colors: 35 color = color.strip() 36 if color.startswith('color-stop('): 37 s, c = split_params(color[11:].rstrip(')')) 38 s = s.strip() 39 c = c.strip() 40 else: 41 c, s = color.split() 42 color_stops.append((to_float(s), c)) 43 return color_stops 44 45 colors = [] 46 stops = [] 47 prev_color = False 48 for c in args: 49 for c in List.from_maybe(c): 50 if _is_color(c): 51 if prev_color: 52 stops.append(None) 53 colors.append(c) 54 prev_color = True 55 elif isinstance(c, Number): 56 stops.append(c) 57 prev_color = False 58 59 if prev_color: 60 stops.append(None) 61 stops = stops[:len(colors)] 62 if stops[0] is None: 63 stops[0] = Number(0, '%') 64 if stops[-1] is None: 65 stops[-1] = Number(100, '%') 66 67 maxable_stops = [s for s in stops if s and not s.is_simple_unit('%')] 68 if maxable_stops: 69 max_stops = max(maxable_stops) 70 else: 71 max_stops = None 72 73 stops = [_s / max_stops if _s and not _s.is_simple_unit('%') else _s for _s in stops] 74 75 init = 0 76 start = None 77 for i, s in enumerate(stops + [1.0]): 78 if s is None: 79 if start is None: 80 start = i 81 end = i 82 else: 83 final = s 84 if start is not None: 85 stride = (final - init) / Number(end - start + 1 + (1 if i < len(stops) else 0)) 86 for j in range(start, end + 1): 87 stops[j] = init + stride * Number(j - start + 1) 88 init = final 89 start = None 90 91 if not max_stops or percentages: 92 pass 93 else: 94 stops = [s if s.is_simple_unit('%') else s * max_stops for s in stops] 95 96 return List(List(pair) for pair in zip(stops, colors)) 97 98 99def _render_standard_color_stops(color_stops): 100 pairs = [] 101 for i, (stop, color) in enumerate(color_stops): 102 if ((i == 0 and stop == Number(0, '%')) or 103 (i == len(color_stops) - 1 and stop == Number(100, '%'))): 104 pairs.append(color) 105 else: 106 pairs.append(List([color, stop], use_comma=False)) 107 108 return List(pairs, use_comma=True) 109 110 111@ns.declare 112def grad_color_stops(*args): 113 args = List.from_maybe_starargs(args) 114 color_stops = __color_stops(True, *args) 115 ret = ', '.join(['color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops]) 116 return String.unquoted(ret) 117 118 119def __grad_end_position(radial, color_stops): 120 return __grad_position(-1, 100, radial, color_stops) 121 122 123@ns.declare 124def grad_point(*p): 125 pos = set() 126 hrz = vrt = Number(0.5, '%') 127 for _p in p: 128 pos.update(String.unquoted(_p).value.split()) 129 if 'left' in pos: 130 hrz = Number(0, '%') 131 elif 'right' in pos: 132 hrz = Number(1, '%') 133 if 'top' in pos: 134 vrt = Number(0, '%') 135 elif 'bottom' in pos: 136 vrt = Number(1, '%') 137 return List([v for v in (hrz, vrt) if v is not None]) 138 139 140def __grad_position(index, default, radial, color_stops): 141 try: 142 stops = Number(color_stops[index][0]) 143 if radial and not stops.is_simple_unit('px') and (index == 0 or index == -1 or index == len(color_stops) - 1): 144 log.warn("Webkit only supports pixels for the start and end stops for radial gradients. Got %s", stops) 145 except IndexError: 146 stops = Number(default) 147 return stops 148 149 150@ns.declare 151def grad_end_position(*color_stops): 152 color_stops = __color_stops(False, *color_stops) 153 return Number(__grad_end_position(False, color_stops)) 154 155 156@ns.declare 157def color_stops(*args): 158 args = List.from_maybe_starargs(args) 159 color_stops = __color_stops(False, *args) 160 ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) 161 return String.unquoted(ret) 162 163 164@ns.declare 165def color_stops_in_percentages(*args): 166 args = List.from_maybe_starargs(args) 167 color_stops = __color_stops(True, *args) 168 ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) 169 return String.unquoted(ret) 170 171 172def _get_gradient_position_and_angle(args): 173 for arg in args: 174 ret = None 175 skip = False 176 for a in arg: 177 if _is_color(a): 178 skip = True 179 break 180 elif isinstance(a, Number): 181 ret = arg 182 if skip: 183 continue 184 if ret is not None: 185 return ret 186 for seek in ( 187 'center', 188 'top', 'bottom', 189 'left', 'right', 190 ): 191 if String(seek) in arg: 192 return arg 193 return None 194 195 196def _get_gradient_shape_and_size(args): 197 for arg in args: 198 for seek in ( 199 'circle', 'ellipse', 200 'closest-side', 'closest-corner', 201 'farthest-side', 'farthest-corner', 202 'contain', 'cover', 203 ): 204 if String(seek) in arg: 205 return arg 206 return None 207 208 209def _get_gradient_color_stops(args): 210 color_stops = [] 211 for arg in args: 212 for a in List.from_maybe(arg): 213 if _is_color(a): 214 color_stops.append(arg) 215 break 216 return color_stops or None 217 218 219# TODO these functions need to be 220# 1. well-defined 221# 2. guaranteed to never wreck css3 syntax 222# 3. updated to whatever current compass does 223# 4. fixed to use a custom type instead of monkeypatching 224 225 226@ns.declare 227def radial_gradient(*args): 228 args = List.from_maybe_starargs(args) 229 230 try: 231 # Do a rough check for standard syntax first -- `shape at position` 232 at_position = list(args[0]).index(String('at')) 233 except (IndexError, ValueError): 234 shape_and_size = _get_gradient_shape_and_size(args) 235 position_and_angle = _get_gradient_position_and_angle(args) 236 else: 237 shape_and_size = List.maybe_new(args[0][:at_position]) 238 position_and_angle = List.maybe_new(args[0][at_position + 1:]) 239 240 color_stops = _get_gradient_color_stops(args) 241 if color_stops is None: 242 raise Exception('No color stops provided to radial-gradient function') 243 color_stops = __color_stops(False, *color_stops) 244 245 if position_and_angle: 246 rendered_position = position(position_and_angle) 247 else: 248 rendered_position = None 249 rendered_color_stops = _render_standard_color_stops(color_stops) 250 251 args = [] 252 if shape_and_size and rendered_position: 253 args.append(List([shape_and_size, String.unquoted('at'), rendered_position], use_comma=False)) 254 elif rendered_position: 255 args.append(rendered_position) 256 elif shape_and_size: 257 args.append(shape_and_size) 258 args.extend(rendered_color_stops) 259 260 legacy_args = [] 261 if rendered_position: 262 legacy_args.append(rendered_position) 263 if shape_and_size: 264 legacy_args.append(shape_and_size) 265 legacy_args.extend(rendered_color_stops) 266 267 ret = String.unquoted( 268 'radial-gradient(' + ', '.join(a.render() for a in args) + ')') 269 270 legacy_ret = 'radial-gradient(' + ', '.join(a.render() for a in legacy_args) + ')' 271 272 def to__css2(): 273 return String.unquoted('') 274 ret.to__css2 = to__css2 275 276 def to__moz(): 277 return String.unquoted('-moz-' + legacy_ret) 278 ret.to__moz = to__moz 279 280 def to__pie(): 281 log.warn("PIE does not support radial-gradient.") 282 return String.unquoted('-pie-radial-gradient(unsupported)') 283 ret.to__pie = to__pie 284 285 def to__webkit(): 286 return String.unquoted('-webkit-' + legacy_ret) 287 ret.to__webkit = to__webkit 288 289 def to__owg(): 290 args = [ 291 'radial', 292 grad_point(*position_and_angle) if position_and_angle is not None else 'center', 293 '0', 294 grad_point(*position_and_angle) if position_and_angle is not None else 'center', 295 __grad_end_position(True, color_stops), 296 ] 297 args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops) 298 ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' 299 return String.unquoted(ret) 300 ret.to__owg = to__owg 301 302 def to__svg(): 303 return radial_svg_gradient(*(list(color_stops) + list(position_and_angle or [String('center')]))) 304 ret.to__svg = to__svg 305 306 return ret 307 308 309@ns.declare 310def linear_gradient(*args): 311 args = List.from_maybe_starargs(args) 312 313 position_and_angle = _get_gradient_position_and_angle(args) 314 color_stops = _get_gradient_color_stops(args) 315 if color_stops is None: 316 raise Exception('No color stops provided to linear-gradient function') 317 color_stops = __color_stops(False, *color_stops) 318 319 args = [ 320 position(position_and_angle) if position_and_angle is not None else None, 321 ] 322 args.extend(_render_standard_color_stops(color_stops)) 323 324 to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' 325 ret = String.unquoted(to__s) 326 327 def to__css2(): 328 return String.unquoted('') 329 ret.to__css2 = to__css2 330 331 def to__moz(): 332 return String.unquoted('-moz-' + to__s) 333 ret.to__moz = to__moz 334 335 def to__pie(): 336 return String.unquoted('-pie-' + to__s) 337 ret.to__pie = to__pie 338 339 def to__ms(): 340 return String.unquoted('-ms-' + to__s) 341 ret.to__ms = to__ms 342 343 def to__o(): 344 return String.unquoted('-o-' + to__s) 345 ret.to__o = to__o 346 347 def to__webkit(): 348 return String.unquoted('-webkit-' + to__s) 349 ret.to__webkit = to__webkit 350 351 def to__owg(): 352 args = [ 353 'linear', 354 position(position_and_angle or None), 355 opposite_position(position_and_angle or None), 356 ] 357 args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops) 358 ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args if a is not None) + ')' 359 return String.unquoted(ret) 360 ret.to__owg = to__owg 361 362 def to__svg(): 363 return linear_svg_gradient(color_stops, position_and_angle or 'top') 364 ret.to__svg = to__svg 365 366 return ret 367 368 369@ns.declare 370def radial_svg_gradient(*args): 371 args = List.from_maybe_starargs(args) 372 color_stops = args 373 center = None 374 if isinstance(args[-1], (String, Number)): 375 center = args[-1] 376 color_stops = args[:-1] 377 color_stops = __color_stops(False, *color_stops) 378 cx, cy = grad_point(center) 379 r = __grad_end_position(True, color_stops) 380 svg = __radial_svg(color_stops, cx, cy, r) 381 url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) 382 inline = 'url("%s")' % escape(url) 383 return String.unquoted(inline) 384 385 386@ns.declare 387def linear_svg_gradient(*args): 388 args = List.from_maybe_starargs(args) 389 color_stops = args 390 start = None 391 if isinstance(args[-1], (String, Number)): 392 start = args[-1] 393 color_stops = args[:-1] 394 color_stops = __color_stops(False, *color_stops) 395 x1, y1 = grad_point(start) 396 x2, y2 = grad_point(opposite_position(start)) 397 svg = _linear_svg(color_stops, x1, y1, x2, y2) 398 url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) 399 inline = 'url("%s")' % escape(url) 400 return String.unquoted(inline) 401 402 403def __color_stops_svg(color_stops): 404 ret = ''.join('<stop offset="%s" stop-color="%s"/>' % (to_str(s), c) for s, c in color_stops) 405 return ret 406 407 408def __svg_template(gradient): 409 ret = '<?xml version="1.0" encoding="utf-8"?>\ 410<svg version="1.1" xmlns="http://www.w3.org/2000/svg">\ 411<defs>%s</defs>\ 412<rect x="0" y="0" width="100%%" height="100%%" fill="url(#grad)" />\ 413</svg>' % gradient 414 return ret 415 416 417def _linear_svg(color_stops, x1, y1, x2, y2): 418 gradient = '<linearGradient id="grad" x1="%s" y1="%s" x2="%s" y2="%s">%s</linearGradient>' % ( 419 to_str(Number(x1)), 420 to_str(Number(y1)), 421 to_str(Number(x2)), 422 to_str(Number(y2)), 423 __color_stops_svg(color_stops) 424 ) 425 return __svg_template(gradient) 426 427 428def __radial_svg(color_stops, cx, cy, r): 429 gradient = '<radialGradient id="grad" gradientUnits="userSpaceOnUse" cx="%s" cy="%s" r="%s">%s</radialGradient>' % ( 430 to_str(Number(cx)), 431 to_str(Number(cy)), 432 to_str(Number(r)), 433 __color_stops_svg(color_stops) 434 ) 435 return __svg_template(gradient) 436