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