lib.graphics
1from typing import Literal, Tuple, TypeAlias 2from math import ceil, exp, sqrt 3import random 4import drawBot 5from drawBot import _drawBotDrawingTool as drawBot 6from drawBot import drawBotDrawingTools 7from lib import layout, color 8from icecream import ic 9 10BlendMode: TypeAlias = Literal[ 11 "normal", 12 "multiply", 13 "screen", 14 "overlay", 15 "darken", 16 "lighten", 17 "colorDodge", 18 "colorBurn", 19 "softLight", 20 "hardLight", 21 "difference", 22 "exclusion", 23 "hue", 24 "saturation", 25 "color", 26 "luminosity", 27 "clear", 28 "copy", 29 "sourceIn", 30 "sourceOut", 31 "sourceAtop", 32 "destinationOver", 33 "destinationIn", 34 "destinationOut", 35 "destinationAtop", 36 "xOR", 37 "plusDarker", 38 "plusLighter", 39] 40"""Supported blend modes. 41 42- taken from https://www.drawbot.com/content/color.html#drawBot.blendMode""" 43 44 45LineCap = Literal["butt", "round"] 46"""Omitted `square` (rarely used).""" 47 48 49def setBlendMode(mode: BlendMode): 50 """Set the blend mode for drawing operations. 51 52 Args: 53 mode: The blend mode to use. 54 """ 55 drawBot.blendMode(mode) 56 57 58# TODO: Merge with app.drawLogo() that doesnt have shade param 59# - Add black/color mode 60# - Rename `density` to `darkness` 61# - Rename `scale` to `upsample` 62def drawLogo( 63 size=200, 64 lineWidth=10, 65 sharpness=0.95, 66 shade: tuple | color.HSL | color.CMYK = (0,), 67 density=0.5, 68 scale=2, 69) -> drawBotDrawingTools.ImageObject: 70 """ 71 Draw a logo image with customizable parameters. 72 73 Args: 74 size: The base size of the logo. 75 lineWidth: The width of the lines in the logo. 76 sharpness: The sharpness of the lines. 77 shade: The fill color or shade. 78 density: Increase float to darken. 79 scale: The upsample scale factor. 80 81 Returns: 82 ImageObject: The generated logo image. 83 """ 84 imgComet = drawBot.ImageObject() 85 86 # Image 1: Comet 87 with imgComet: 88 size *= scale 89 drawBot.size(size, size) 90 center = (size / 2,) * 2 91 drawBot.radialGradient( 92 center, 93 center, 94 colors=[(1 - density,) * 3, (1,) * 3], 95 locations=[0, 1], 96 startRadius=size / 4, 97 endRadius=size / 2, 98 ) 99 drawBot.rect(0, 0, size, size) 100 101 imgComet.lineScreen(width=lineWidth * scale, sharpness=sharpness) 102 imgComet.colorInvert() 103 104 # Image 2: Solid 105 imgSolid = drawBot.ImageObject() 106 with imgSolid: 107 drawBot.size(*imgComet.size()) 108 if isinstance(shade, (color.HSL, color.CMYK)): 109 shade.setFill() 110 else: 111 drawBot.fill(*shade) 112 drawBot.rect(0, 0, *imgComet.size()) 113 114 imgSolid.blendWithMask(maskImage=imgComet) 115 imgSolid.lanczosScaleTransform(scale=1 / scale) 116 117 return imgSolid 118 119 120def renderClouds( 121 dimensions: tuple, zoom=150, dot=10, scale=1, brightness=0, contrast=1.5 122) -> drawBotDrawingTools.ImageObject: 123 """Render a cloud-like image with random dot patterns. 124 125 Args: 126 dimensions: The width and height of the output image. 127 zoom: The zoom factor for the dot pattern. 128 dot: The size of the dots. 129 scale: The scale factor for the inner image. 130 brightness: Brightness adjustment. 131 contrast: Contrast adjustment. 132 133 Returns: 134 ImageObject: The generated cloud image. 135 """ 136 imgOuter = drawBot.ImageObject() 137 w, h = layout.toDimensions(dimensions) 138 139 with imgOuter: 140 drawBot.size(w, h) 141 drawBot.fill(0.9) 142 drawBot.rect(0, 0, w, h) 143 144 imgInner = drawBot.ImageObject() 145 with imgInner: 146 drawBot.size(*[dim * scale for dim in [w, h]]) 147 imgInner.randomGenerator([ceil(dim / zoom) for dim in imgInner.size()]) 148 imgInner.lanczosScaleTransform(zoom) 149 150 shiftPos = shiftWithin(dimensions, imgInner) 151 drawBot.image(imgInner, shiftPos) 152 153 imgOuter.colorControls(brightness=brightness, contrast=contrast) 154 imgOuter.dotScreen(width=zoom / dot, angle=45, sharpness=1) 155 imgOuter.colorInvert() 156 157 return imgOuter 158 159 160def shiftWithin( 161 parent: tuple, child: drawBotDrawingTools.ImageObject 162) -> tuple[int, int]: 163 """Return random `x, y` position within parent. 164 165 Args: 166 parent: The dimensions of the parent container. 167 child: The image object to position. 168 169 Returns: 170 tuple[int, int]: The random (x, y) position. 171 """ 172 parentW, parentH = layout.toDimensions(parent) 173 childW, childH = child.size() 174 175 difW, difH = parentW - childW, parentH - childH 176 return [random.random() * (dif / 2) for dif in [difW, difH]] 177 178 179def sampleLuminosity( 180 image_path: str, 181 cell_x: float, 182 cell_y: float, 183 cell_width: float, 184 cell_height: float, 185 sample_points: int = 9, 186) -> float: 187 """ 188 Sample luminosity from multiple points within a cell for better accuracy 189 190 Args: 191 image_path: Path to the image 192 cell_x, cell_y: Top-left corner of the cell 193 cell_width, cell_height: Cell dimensions 194 sample_points: Number of sample points (will be made into a square grid) 195 """ 196 luminosity_sum = 0 197 valid_samples = 0 198 199 # Calculate grid size (make it square) 200 grid_size = int(sqrt(sample_points)) 201 202 # Create sample points in a grid within the cell 203 for i in range(grid_size): 204 for j in range(grid_size): 205 # Calculate sample position within the cell 206 sample_x = cell_x + (i + 0.5) * cell_width / grid_size 207 sample_y = cell_y + (j + 0.5) * cell_height / grid_size 208 209 # Sample pixel color at this point 210 pixel_data = drawBot.imagePixelColor(image_path, (sample_x, sample_y)) 211 212 if pixel_data: 213 r, g, b, _ = pixel_data 214 luminosity = 0.299 * r + 0.587 * g + 0.114 * b 215 luminosity_sum += luminosity 216 valid_samples += 1 217 218 # Round to 10 decimals (arbitrary) to fix floating point imprecision 219 return round(luminosity_sum / valid_samples, 10) if valid_samples > 0 else 0 220 221 222def adjustContrast(luminosity: float, contrast: float = 1.0): 223 """ 224 S-curve contrast adjustment using sigmoid function 225 - < 1.0: reduces contrast 226 - = 1.0: no change 227 - > 1.0: increases contrast 228 """ 229 if contrast == 0: 230 return 0.5 # Gray 231 elif contrast == 1: 232 return luminosity # Unchanged 233 234 # Convert to range that works well with sigmoid 235 x = (luminosity - 0.5) * contrast 236 # Apply sigmoid function 237 sigmoid = 1 / (1 + exp(-x * 6)) # 6 is steepness factor 238 return sigmoid 239 240 241def renderLineScreen( 242 imagePath: str | drawBotDrawingTools.ImageObject, 243 position: Tuple[float, float], 244 rows: int, 245 cols: int, 246 bleed: Tuple[float, float] = (0, 0), 247 sampleOffset: Tuple[float, float] = (0, 0), 248 contrast: float = 1.0, 249 samplePoints: int = 9, 250 lineCap: LineCap = "butt", 251 minThickness: float = 0.1, 252) -> None: 253 """ 254 Karel Martens-style line screen effect 255 256 Args: 257 imagePath: Path to input image 258 rows: Number of grid rows 259 cols: Number of grid columns 260 bleed: Tuple (x, y) - amount to extend cells in each direction 261 sampleOffset: Tuple (x, y) - offset for sampling luminosity 262 contrast: Contrast adjustment factor 263 - < 1.0: reduces contrast 264 - = 1.0: no change 265 - > 1.0: increases contrast 266 samplePoints: Number of points to sample for luminosity calculation 267 lineCap: Line cap style, either "butt" or "round" 268 minThickness: Minimum stroke thickness in pts 269 """ 270 271 posX, posY = position 272 imgW, imgH = drawBot.imageSize(imagePath) 273 cellW, cellH = imgW / cols, imgH / rows 274 275 # Extract bleed values 276 bleedX, bleedY = bleed 277 sampleOffsetX, sampleOffsetY = sampleOffset 278 279 # White background 280 drawBot.fill(1) 281 drawBot.rect(0, 0, imgW, imgH) 282 283 # Black lines 284 drawBot.fill(None) 285 drawBot.stroke(0) 286 drawBot.lineCap(lineCap) 287 288 for row in range(rows): 289 for col in range(cols): 290 # Original cell position 291 x = col * cellW 292 y = (rows - 1 - row) * cellH 293 294 # Clamp coordinates to image bounds before sampling luminosity 295 sampleX = max(0, min(x + sampleOffsetX, imgW - (cellW / 2))) 296 sampleY = max(0, min(y + sampleOffsetY, imgH - (cellH / 2))) 297 298 luminosity = sampleLuminosity( 299 imagePath, 300 sampleX, 301 sampleY, 302 cellW, 303 cellH, 304 samplePoints, 305 ) 306 307 luminosity = adjustContrast(luminosity, contrast) 308 309 # Map to thickness (invert for dark = thick) 310 thicknessRatio = 1 - luminosity 311 lineW = max(minThickness, thicknessRatio * cellW) 312 313 # Center the line in original cell 314 lineX = x + (cellW - lineW) / 2 315 316 # Apply bleed to extend the drawing area 317 extX = x - bleedX 318 extY = y - bleedY 319 extW = cellW + (2 * bleedX) 320 extH = cellH + (2 * bleedY) 321 322 # Extend line width with horizontal bleed 323 extLineX = lineX - bleedX 324 extLineW = lineW + (2 * bleedX) 325 326 # Clamp to canvas boundaries 327 if ( 328 lineW > 0 329 and extX < imgW 330 and extY < imgH 331 and extX + extW > 0 332 and extY + extH > 0 333 ): 334 # Calculate actual drawing bounds 335 actualX = max(0, extX) 336 actualY = max(0, extY) 337 actualW = min(imgW - actualX, extW - (actualX - extX)) 338 actualH = min(imgH - actualY, extH - (actualY - extY)) 339 340 # Calculate actual line bounds 341 actualLineX = max(0, extLineX) 342 actualLineW = min( 343 imgW - actualLineX, 344 extLineW - (actualLineX - extLineX), 345 ) 346 347 # Draw vertical line with bleed 348 if actualLineW > 0 and actualW > 0 and actualH > 0: 349 drawBot.strokeWidth(lineW) 350 drawBot.line( 351 (posX + actualLineX + lineW / 2, posY + actualY), 352 ( 353 posX + actualLineX + lineW / 2, 354 posY + actualY + actualH, 355 ), 356 )
BlendMode: TypeAlias =
Literal['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'colorDodge', 'colorBurn', 'softLight', 'hardLight', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity', 'clear', 'copy', 'sourceIn', 'sourceOut', 'sourceAtop', 'destinationOver', 'destinationIn', 'destinationOut', 'destinationAtop', 'xOR', 'plusDarker', 'plusLighter']
Supported blend modes.
LineCap =
typing.Literal['butt', 'round']
Omitted square (rarely used).
def
setBlendMode( mode: Literal['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'colorDodge', 'colorBurn', 'softLight', 'hardLight', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity', 'clear', 'copy', 'sourceIn', 'sourceOut', 'sourceAtop', 'destinationOver', 'destinationIn', 'destinationOut', 'destinationAtop', 'xOR', 'plusDarker', 'plusLighter']):
50def setBlendMode(mode: BlendMode): 51 """Set the blend mode for drawing operations. 52 53 Args: 54 mode: The blend mode to use. 55 """ 56 drawBot.blendMode(mode)
Set the blend mode for drawing operations.
Arguments:
- mode: The blend mode to use.
def
drawLogo( size=200, lineWidth=10, sharpness=0.95, shade: tuple | lib.color.HSL | lib.color.CMYK = (0,), density=0.5, scale=2) -> drawBot.context.tools.imageObject.ImageObject:
63def drawLogo( 64 size=200, 65 lineWidth=10, 66 sharpness=0.95, 67 shade: tuple | color.HSL | color.CMYK = (0,), 68 density=0.5, 69 scale=2, 70) -> drawBotDrawingTools.ImageObject: 71 """ 72 Draw a logo image with customizable parameters. 73 74 Args: 75 size: The base size of the logo. 76 lineWidth: The width of the lines in the logo. 77 sharpness: The sharpness of the lines. 78 shade: The fill color or shade. 79 density: Increase float to darken. 80 scale: The upsample scale factor. 81 82 Returns: 83 ImageObject: The generated logo image. 84 """ 85 imgComet = drawBot.ImageObject() 86 87 # Image 1: Comet 88 with imgComet: 89 size *= scale 90 drawBot.size(size, size) 91 center = (size / 2,) * 2 92 drawBot.radialGradient( 93 center, 94 center, 95 colors=[(1 - density,) * 3, (1,) * 3], 96 locations=[0, 1], 97 startRadius=size / 4, 98 endRadius=size / 2, 99 ) 100 drawBot.rect(0, 0, size, size) 101 102 imgComet.lineScreen(width=lineWidth * scale, sharpness=sharpness) 103 imgComet.colorInvert() 104 105 # Image 2: Solid 106 imgSolid = drawBot.ImageObject() 107 with imgSolid: 108 drawBot.size(*imgComet.size()) 109 if isinstance(shade, (color.HSL, color.CMYK)): 110 shade.setFill() 111 else: 112 drawBot.fill(*shade) 113 drawBot.rect(0, 0, *imgComet.size()) 114 115 imgSolid.blendWithMask(maskImage=imgComet) 116 imgSolid.lanczosScaleTransform(scale=1 / scale) 117 118 return imgSolid
Draw a logo image with customizable parameters.
Arguments:
- size: The base size of the logo.
- lineWidth: The width of the lines in the logo.
- sharpness: The sharpness of the lines.
- shade: The fill color or shade.
- density: Increase float to darken.
- scale: The upsample scale factor.
Returns:
ImageObject: The generated logo image.
def
renderClouds( dimensions: tuple, zoom=150, dot=10, scale=1, brightness=0, contrast=1.5) -> drawBot.context.tools.imageObject.ImageObject:
121def renderClouds( 122 dimensions: tuple, zoom=150, dot=10, scale=1, brightness=0, contrast=1.5 123) -> drawBotDrawingTools.ImageObject: 124 """Render a cloud-like image with random dot patterns. 125 126 Args: 127 dimensions: The width and height of the output image. 128 zoom: The zoom factor for the dot pattern. 129 dot: The size of the dots. 130 scale: The scale factor for the inner image. 131 brightness: Brightness adjustment. 132 contrast: Contrast adjustment. 133 134 Returns: 135 ImageObject: The generated cloud image. 136 """ 137 imgOuter = drawBot.ImageObject() 138 w, h = layout.toDimensions(dimensions) 139 140 with imgOuter: 141 drawBot.size(w, h) 142 drawBot.fill(0.9) 143 drawBot.rect(0, 0, w, h) 144 145 imgInner = drawBot.ImageObject() 146 with imgInner: 147 drawBot.size(*[dim * scale for dim in [w, h]]) 148 imgInner.randomGenerator([ceil(dim / zoom) for dim in imgInner.size()]) 149 imgInner.lanczosScaleTransform(zoom) 150 151 shiftPos = shiftWithin(dimensions, imgInner) 152 drawBot.image(imgInner, shiftPos) 153 154 imgOuter.colorControls(brightness=brightness, contrast=contrast) 155 imgOuter.dotScreen(width=zoom / dot, angle=45, sharpness=1) 156 imgOuter.colorInvert() 157 158 return imgOuter
Render a cloud-like image with random dot patterns.
Arguments:
- dimensions: The width and height of the output image.
- zoom: The zoom factor for the dot pattern.
- dot: The size of the dots.
- scale: The scale factor for the inner image.
- brightness: Brightness adjustment.
- contrast: Contrast adjustment.
Returns:
ImageObject: The generated cloud image.
def
shiftWithin( parent: tuple, child: drawBot.context.tools.imageObject.ImageObject) -> tuple[int, int]:
161def shiftWithin( 162 parent: tuple, child: drawBotDrawingTools.ImageObject 163) -> tuple[int, int]: 164 """Return random `x, y` position within parent. 165 166 Args: 167 parent: The dimensions of the parent container. 168 child: The image object to position. 169 170 Returns: 171 tuple[int, int]: The random (x, y) position. 172 """ 173 parentW, parentH = layout.toDimensions(parent) 174 childW, childH = child.size() 175 176 difW, difH = parentW - childW, parentH - childH 177 return [random.random() * (dif / 2) for dif in [difW, difH]]
Return random x, y position within parent.
Arguments:
- parent: The dimensions of the parent container.
- child: The image object to position.
Returns:
tuple[int, int]: The random (x, y) position.
def
sampleLuminosity( image_path: str, cell_x: float, cell_y: float, cell_width: float, cell_height: float, sample_points: int = 9) -> float:
180def sampleLuminosity( 181 image_path: str, 182 cell_x: float, 183 cell_y: float, 184 cell_width: float, 185 cell_height: float, 186 sample_points: int = 9, 187) -> float: 188 """ 189 Sample luminosity from multiple points within a cell for better accuracy 190 191 Args: 192 image_path: Path to the image 193 cell_x, cell_y: Top-left corner of the cell 194 cell_width, cell_height: Cell dimensions 195 sample_points: Number of sample points (will be made into a square grid) 196 """ 197 luminosity_sum = 0 198 valid_samples = 0 199 200 # Calculate grid size (make it square) 201 grid_size = int(sqrt(sample_points)) 202 203 # Create sample points in a grid within the cell 204 for i in range(grid_size): 205 for j in range(grid_size): 206 # Calculate sample position within the cell 207 sample_x = cell_x + (i + 0.5) * cell_width / grid_size 208 sample_y = cell_y + (j + 0.5) * cell_height / grid_size 209 210 # Sample pixel color at this point 211 pixel_data = drawBot.imagePixelColor(image_path, (sample_x, sample_y)) 212 213 if pixel_data: 214 r, g, b, _ = pixel_data 215 luminosity = 0.299 * r + 0.587 * g + 0.114 * b 216 luminosity_sum += luminosity 217 valid_samples += 1 218 219 # Round to 10 decimals (arbitrary) to fix floating point imprecision 220 return round(luminosity_sum / valid_samples, 10) if valid_samples > 0 else 0
Sample luminosity from multiple points within a cell for better accuracy
Arguments:
- image_path: Path to the image
- cell_x, cell_y: Top-left corner of the cell
- cell_width, cell_height: Cell dimensions
- sample_points: Number of sample points (will be made into a square grid)
def
adjustContrast(luminosity: float, contrast: float = 1.0):
223def adjustContrast(luminosity: float, contrast: float = 1.0): 224 """ 225 S-curve contrast adjustment using sigmoid function 226 - < 1.0: reduces contrast 227 - = 1.0: no change 228 - > 1.0: increases contrast 229 """ 230 if contrast == 0: 231 return 0.5 # Gray 232 elif contrast == 1: 233 return luminosity # Unchanged 234 235 # Convert to range that works well with sigmoid 236 x = (luminosity - 0.5) * contrast 237 # Apply sigmoid function 238 sigmoid = 1 / (1 + exp(-x * 6)) # 6 is steepness factor 239 return sigmoid
S-curve contrast adjustment using sigmoid function
- < 1.0: reduces contrast
- = 1.0: no change
- > 1.0: increases contrast
def
renderLineScreen( imagePath: str | drawBot.context.tools.imageObject.ImageObject, position: Tuple[float, float], rows: int, cols: int, bleed: Tuple[float, float] = (0, 0), sampleOffset: Tuple[float, float] = (0, 0), contrast: float = 1.0, samplePoints: int = 9, lineCap: Literal['butt', 'round'] = 'butt', minThickness: float = 0.1) -> None:
242def renderLineScreen( 243 imagePath: str | drawBotDrawingTools.ImageObject, 244 position: Tuple[float, float], 245 rows: int, 246 cols: int, 247 bleed: Tuple[float, float] = (0, 0), 248 sampleOffset: Tuple[float, float] = (0, 0), 249 contrast: float = 1.0, 250 samplePoints: int = 9, 251 lineCap: LineCap = "butt", 252 minThickness: float = 0.1, 253) -> None: 254 """ 255 Karel Martens-style line screen effect 256 257 Args: 258 imagePath: Path to input image 259 rows: Number of grid rows 260 cols: Number of grid columns 261 bleed: Tuple (x, y) - amount to extend cells in each direction 262 sampleOffset: Tuple (x, y) - offset for sampling luminosity 263 contrast: Contrast adjustment factor 264 - < 1.0: reduces contrast 265 - = 1.0: no change 266 - > 1.0: increases contrast 267 samplePoints: Number of points to sample for luminosity calculation 268 lineCap: Line cap style, either "butt" or "round" 269 minThickness: Minimum stroke thickness in pts 270 """ 271 272 posX, posY = position 273 imgW, imgH = drawBot.imageSize(imagePath) 274 cellW, cellH = imgW / cols, imgH / rows 275 276 # Extract bleed values 277 bleedX, bleedY = bleed 278 sampleOffsetX, sampleOffsetY = sampleOffset 279 280 # White background 281 drawBot.fill(1) 282 drawBot.rect(0, 0, imgW, imgH) 283 284 # Black lines 285 drawBot.fill(None) 286 drawBot.stroke(0) 287 drawBot.lineCap(lineCap) 288 289 for row in range(rows): 290 for col in range(cols): 291 # Original cell position 292 x = col * cellW 293 y = (rows - 1 - row) * cellH 294 295 # Clamp coordinates to image bounds before sampling luminosity 296 sampleX = max(0, min(x + sampleOffsetX, imgW - (cellW / 2))) 297 sampleY = max(0, min(y + sampleOffsetY, imgH - (cellH / 2))) 298 299 luminosity = sampleLuminosity( 300 imagePath, 301 sampleX, 302 sampleY, 303 cellW, 304 cellH, 305 samplePoints, 306 ) 307 308 luminosity = adjustContrast(luminosity, contrast) 309 310 # Map to thickness (invert for dark = thick) 311 thicknessRatio = 1 - luminosity 312 lineW = max(minThickness, thicknessRatio * cellW) 313 314 # Center the line in original cell 315 lineX = x + (cellW - lineW) / 2 316 317 # Apply bleed to extend the drawing area 318 extX = x - bleedX 319 extY = y - bleedY 320 extW = cellW + (2 * bleedX) 321 extH = cellH + (2 * bleedY) 322 323 # Extend line width with horizontal bleed 324 extLineX = lineX - bleedX 325 extLineW = lineW + (2 * bleedX) 326 327 # Clamp to canvas boundaries 328 if ( 329 lineW > 0 330 and extX < imgW 331 and extY < imgH 332 and extX + extW > 0 333 and extY + extH > 0 334 ): 335 # Calculate actual drawing bounds 336 actualX = max(0, extX) 337 actualY = max(0, extY) 338 actualW = min(imgW - actualX, extW - (actualX - extX)) 339 actualH = min(imgH - actualY, extH - (actualY - extY)) 340 341 # Calculate actual line bounds 342 actualLineX = max(0, extLineX) 343 actualLineW = min( 344 imgW - actualLineX, 345 extLineW - (actualLineX - extLineX), 346 ) 347 348 # Draw vertical line with bleed 349 if actualLineW > 0 and actualW > 0 and actualH > 0: 350 drawBot.strokeWidth(lineW) 351 drawBot.line( 352 (posX + actualLineX + lineW / 2, posY + actualY), 353 ( 354 posX + actualLineX + lineW / 2, 355 posY + actualY + actualH, 356 ), 357 )
Karel Martens-style line screen effect
Arguments:
- imagePath: Path to input image
- rows: Number of grid rows
- cols: Number of grid columns
- bleed: Tuple (x, y) - amount to extend cells in each direction
- sampleOffset: Tuple (x, y) - offset for sampling luminosity
- contrast: Contrast adjustment factor
- < 1.0: reduces contrast
- = 1.0: no change
- > 1.0: increases contrast
- samplePoints: Number of points to sample for luminosity calculation
- lineCap: Line cap style, either "butt" or "round"
- minThickness: Minimum stroke thickness in pts