File indexing completed on 2025-01-05 04:00:26
0001 #!/usr/bin/env python3 0002 0003 import os 0004 import sys 0005 import math 0006 import argparse 0007 from PIL import Image 0008 sys.path.insert(0, os.path.join( 0009 os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 0010 "lib" 0011 )) 0012 from lottie.utils.color import Color, ColorMode 0013 from lottie.parsers.svg.importer import parse_color 0014 0015 0016 parser = argparse.ArgumentParser( 0017 description="Visualizes color spaces", 0018 conflict_handler="resolve", 0019 ) 0020 parser.add_argument( 0021 "--width", "-w", 0022 default=512, 0023 type=int, 0024 help="Width of the output image", 0025 ) 0026 parser.add_argument( 0027 "--height", 0028 "-h", 0029 default=512, 0030 type=int, 0031 help="Height of the output image", 0032 ) 0033 0034 g = parser.add_mutually_exclusive_group() 0035 g.add_argument( 0036 "--base", 0037 "-b", 0038 type=parse_color, 0039 default=Color(1, 0, 0), 0040 help="Base color", 0041 ) 0042 g.add_argument( 0043 "--other", 0044 type=float, 0045 default=None, 0046 help="Value of the other component", 0047 ) 0048 0049 parser.add_argument( 0050 "--radial", 0051 action="store_true", 0052 help="Whether to draw a circle rather than a square" 0053 ) 0054 parser.add_argument( 0055 "space", 0056 choices=list(ColorMode.__members__.keys()), 0057 help="Color space" 0058 ) 0059 0060 parser.add_argument( 0061 "--min", 0062 "-m", 0063 default=None, 0064 type=float, 0065 help="Minimum value to show for `component`" 0066 ) 0067 parser.add_argument( 0068 "--max", 0069 "-M", 0070 default=None, 0071 type=float, 0072 help="Maximum value to show for `component`" 0073 ) 0074 parser.add_argument("component", help="Component to show") 0075 0076 0077 parser.add_argument( 0078 "--min2", 0079 "-m2", 0080 default=None, 0081 type=float, 0082 help="Minimum value to show for `component2`" 0083 ) 0084 parser.add_argument( 0085 "--max2", 0086 "-M2", 0087 default=None, 0088 type=float, 0089 help="Maximum value to show for `component2`" 0090 ) 0091 parser.add_argument("component2", help="Optional secondary component to show") 0092 0093 0094 class CompChanger: 0095 def __init__(self, min_v, max_v, component, space): 0096 self.component = component.lower() 0097 0098 self.min_v = min_v 0099 if self.min_v is None: 0100 self.min_v = 0 0101 if space == ColorMode.LAB or space == ColorMode.LUV: 0102 self.min_v = -100 0103 0104 self.max_v = max_v 0105 if self.max_v is None: 0106 self.max_v = 1 0107 if space == ColorMode.LCH_uv and self.component == 'h': 0108 self.max_v = math.tau 0109 elif space == ColorMode.LAB or space == ColorMode.LUV: 0110 self.max_v = 100 0111 0112 def apply(self, color, factor): 0113 comp_v = factor * (self.max_v - self.min_v) + self.min_v 0114 setattr(color, self.component, comp_v) 0115 0116 0117 def color_tuple(color): 0118 return tuple(int(round(c*255)) for c in color.to_rgb()) 0119 0120 0121 if __name__ == "__main__": 0122 ns = parser.parse_args() 0123 0124 space = ColorMode[ns.space] 0125 base_color = ns.base.converted(space) 0126 width = ns.width 0127 height = ns.height 0128 0129 if ns.other is not None and ns.component2: 0130 comps = base_color.component_names() 0131 for comp in comps: 0132 if ns.component.lower() not in comp and ns.component2.lower() not in comp: 0133 break 0134 0135 setattr(base_color, list(comp)[0], ns.other) 0136 0137 comp1 = CompChanger(ns.min, ns.max, ns.component, space) 0138 comp2 = None 0139 if ns.component2: 0140 comp2 = CompChanger(ns.min2, ns.max2, ns.component2, space) 0141 0142 img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) 0143 pixels = img.load() 0144 0145 for x in range(width): 0146 ix = x / (width-1) 0147 sys.stdout.write("\r%d%%" % (ix * 100)) 0148 0149 if not ns.radial: 0150 color_x = base_color.clone() 0151 comp1.apply(color_x, ix) 0152 color_t = color_tuple(color_x) 0153 0154 for y in range(height): 0155 iy = y / (height-1) 0156 0157 if not ns.radial: 0158 if comp2: 0159 color = color_x.clone() 0160 comp2.apply(color, iy) 0161 color_t = color_tuple(color) 0162 else: 0163 xnorm = ix * 2 - 1 0164 ynorm = iy * 2 - 1 0165 angle = math.atan2(ynorm, xnorm) % math.tau / math.tau 0166 distance = math.hypot(xnorm, ynorm) 0167 if distance > 1: 0168 continue 0169 0170 color = base_color.clone() 0171 comp1.apply(color, angle) 0172 if comp2: 0173 comp2.apply(color, distance) 0174 color_t = color_tuple(color) 0175 0176 pixels[x, y] = color_t 0177 0178 img.show() 0179 img.save("/tmp/out.png") 0180