Render shape/text shadows and gradient strokes
- soft offset shadow (shadowDistance/Blur/Angle/Color) for shape and rasterized text output + previews - gradient shape strokes (ring-clipped gradient) instead of black fallback (Note: tl-mode TypeLayer shadows need LayerEffects - not implemented yet)
This commit is contained in:
1 parent
7ee5f6e6a8
commit
724449f3f0
1 file changed
+46
-2
+46
-2
@@ -262,13 +262,48 @@ def render_shape(ly: dict, cw: int, ch: int) -> Optional[Tuple[Image.Image, Tupl
|
||||
draw.rounded_rectangle(bbox, radius=r_, outline=stroke["colors"][0][1],
|
||||
width=thickness)
|
||||
else:
|
||||
draw.rounded_rectangle(bbox, radius=r_, outline=(0, 0, 0, 255),
|
||||
width=thickness)
|
||||
# gradient stroke: paste gradient clipped to a rounded-rect ring
|
||||
stroke_img = make_linear_gradient(ww, hh, stroke)
|
||||
ring = Image.new("L", (ww, hh), 0)
|
||||
ImageDraw.Draw(ring).rounded_rectangle(bbox, radius=r_,
|
||||
outline=255, width=thickness)
|
||||
img.paste(stroke_img, (0, 0), ring)
|
||||
|
||||
apply_opacity(img, ly.get("alpha", 255))
|
||||
# shadow (ppl renders a soft offset shadow per layer)
|
||||
sd = float(ly.get("shadowDistance", "0") or 0)
|
||||
sb = float(ly.get("shadowBlur", "0") or 0)
|
||||
if (sd > 0 or sb > 0) and ly.get("_no_shadow") != "1":
|
||||
img, ox, oy = _add_shadow(img, sd, sb,
|
||||
float(ly.get("shadowAngle", "0") or 0),
|
||||
ly.get("shadowColor", "-16777216"))
|
||||
return img, (ix0 - ox, iy0 - oy)
|
||||
return img, (ix0, iy0)
|
||||
|
||||
|
||||
def _add_shadow(img, distance, blur, angle_deg, color_spec):
|
||||
"""Render a soft offset shadow under the layer content. Returns
|
||||
(expanded_img, offset_x, offset_y)."""
|
||||
import math
|
||||
from PIL import ImageFilter
|
||||
rgba = argb_to_rgba(int(color_spec or "-16777216"))
|
||||
rad = math.radians(angle_deg)
|
||||
dx = int(round(distance * math.cos(rad)))
|
||||
dy = int(round(distance * math.sin(rad)))
|
||||
pad = max(int(blur) * 2 + abs(dx) + abs(dy) + 4, 8)
|
||||
w, h = img.size
|
||||
canvas = Image.new("RGBA", (w + 2 * pad, h + 2 * pad), (0, 0, 0, 0))
|
||||
mask = img.getchannel("A")
|
||||
shadow_fill = Image.new("RGBA", (w, h), rgba)
|
||||
shadow_fill.putalpha(mask)
|
||||
sh_layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
||||
sh_layer.paste(shadow_fill, (pad + dx, pad + dy), mask)
|
||||
sh_layer = sh_layer.filter(ImageFilter.GaussianBlur(blur))
|
||||
canvas.alpha_composite(sh_layer)
|
||||
canvas.alpha_composite(img, (pad, pad))
|
||||
return canvas, pad, pad
|
||||
|
||||
|
||||
def _font_path_for(ly: dict) -> str:
|
||||
spec = ly.get("textFont", "")
|
||||
return resolve_font(spec)
|
||||
@@ -394,6 +429,15 @@ def render_text(ly: dict, cw: int, ch: int) -> Optional[Tuple[Image.Image, Tuple
|
||||
img = img.crop((ix0 - left, iy0 - top, ix1 - left, iy1 - top))
|
||||
|
||||
apply_opacity(img, ly.get("alpha", 255))
|
||||
|
||||
# soft shadow (used by rasterized output / previews)
|
||||
sd = float(ly.get("shadowDistance", "0") or 0)
|
||||
sb = float(ly.get("shadowBlur", "0") or 0)
|
||||
if (sd > 0 or sb > 0) and ly.get("_no_shadow") != "1":
|
||||
img, ox, oy = _add_shadow(img, sd, sb,
|
||||
float(ly.get("shadowAngle", "0") or 0),
|
||||
ly.get("shadowColor", "-16777216"))
|
||||
return img, (ix0 - ox, iy0 - oy)
|
||||
return img, (ix0, iy0)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user