refactor(socialcard): redesign card layouts and add portrait format
All checks were successful
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m46s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Successful in 5m22s
Build and Release / Unit Tests (push) Successful in 5m35s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m7s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m40s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 8m6s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 8m50s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m27s
All checks were successful
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m46s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Successful in 5m22s
Build and Release / Unit Tests (push) Successful in 5m35s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m7s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m40s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 8m6s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 8m50s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m27s
Redesign preset card layout with left accent stripe, centered avatar, and vertically centered text blocks. Add portrait format (1080x1350) for solid and image styles with split layout. Improve text wrapping and truncation logic. Update solid card to support background image in bottom half with solid color top section.
This commit is contained in:
@@ -37,6 +37,10 @@ var logoData []byte
|
||||
const (
|
||||
CardWidth = 1200
|
||||
CardHeight = 630
|
||||
|
||||
// Portrait dimensions for solid and image styles.
|
||||
PortraitWidth = 1080
|
||||
PortraitHeight = 1350
|
||||
)
|
||||
|
||||
// Theme defines the color scheme for a social card.
|
||||
@@ -108,10 +112,15 @@ func IsValidStyle(name string) bool {
|
||||
}
|
||||
|
||||
// GetTheme returns a theme by name, falling back to dark.
|
||||
// For "solid" and "image" styles, returns a placeholder theme with the correct Name
|
||||
// so the RenderCard dispatcher routes to the right renderer.
|
||||
func GetTheme(name string) Theme {
|
||||
if t, ok := ThemesByName[name]; ok {
|
||||
return t
|
||||
}
|
||||
if name == "solid" || name == "image" {
|
||||
return Theme{Name: name}
|
||||
}
|
||||
return ThemeDark
|
||||
}
|
||||
|
||||
@@ -171,8 +180,8 @@ func (r *Renderer) renderPresetCard(data CardData, theme Theme) ([]byte, error)
|
||||
// Fill background
|
||||
fillRect(img, img.Bounds(), theme.Background)
|
||||
|
||||
// Top accent bar (4px)
|
||||
fillRect(img, image.Rect(0, 0, CardWidth, 4), theme.CardBorderColor)
|
||||
// Left accent stripe (6px wide)
|
||||
fillRect(img, image.Rect(0, 0, 6, CardHeight), theme.CardBorderColor)
|
||||
|
||||
faces, err := r.createFaces()
|
||||
if err != nil {
|
||||
@@ -180,47 +189,110 @@ func (r *Renderer) renderPresetCard(data CardData, theme Theme) ([]byte, error)
|
||||
}
|
||||
defer faces.close()
|
||||
|
||||
padding := 60
|
||||
avatarSize := 96
|
||||
avatarGap := 24
|
||||
leftPad := 60
|
||||
rightPad := 60
|
||||
avatarSize := 140
|
||||
avatarGap := 40
|
||||
|
||||
// Repo avatar (top-right, rounded)
|
||||
// Repo avatar (right side, vertically centered, rounded)
|
||||
hasAvatar := false
|
||||
if data.RepoAvatarURL != "" {
|
||||
avatarImg := fetchAndDecodeAvatar(data.RepoAvatarURL, avatarSize)
|
||||
if avatarImg != nil {
|
||||
avatarX := CardWidth - padding - avatarSize
|
||||
avatarY := padding + 20
|
||||
drawRoundedAvatar(img, avatarX, avatarY, avatarSize, 12, avatarImg)
|
||||
avatarX := CardWidth - rightPad - avatarSize
|
||||
avatarY := (CardHeight - avatarSize) / 2
|
||||
drawRoundedAvatar(img, avatarX, avatarY, avatarSize, 16, avatarImg)
|
||||
hasAvatar = true
|
||||
}
|
||||
}
|
||||
|
||||
// Text area width: shrink if avatar is present to avoid overlap
|
||||
contentWidth := CardWidth - 2*padding
|
||||
// Text area width: shrink if avatar is present
|
||||
contentWidth := CardWidth - leftPad - rightPad
|
||||
if hasAvatar {
|
||||
contentWidth = CardWidth - 2*padding - avatarSize - avatarGap
|
||||
contentWidth = CardWidth - leftPad - rightPad - avatarSize - avatarGap
|
||||
}
|
||||
|
||||
r.drawCardText(img, faces, data, theme, padding, contentWidth)
|
||||
// Calculate total text block height for vertical centering
|
||||
titleLines := wrapText(data.Title, faces.title, contentWidth)
|
||||
if len(titleLines) > 2 {
|
||||
titleLines = titleLines[:2]
|
||||
titleLines[1] = truncateWithEllipsis(titleLines[1], faces.title, contentWidth)
|
||||
}
|
||||
titleBlockH := len(titleLines) * 60
|
||||
|
||||
var descLines []string
|
||||
descBlockH := 0
|
||||
if data.Description != "" {
|
||||
descLines = wrapText(data.Description, faces.desc, contentWidth)
|
||||
if len(descLines) > 3 {
|
||||
descLines = descLines[:3]
|
||||
descLines[2] = truncateWithEllipsis(descLines[2], faces.desc, contentWidth)
|
||||
}
|
||||
descBlockH = len(descLines)*34 + 16 // 16 = gap between title and desc
|
||||
}
|
||||
|
||||
metaH := 28 // height of bottom metadata line
|
||||
gapBeforeMeta := 24
|
||||
totalH := titleBlockH + descBlockH + gapBeforeMeta + metaH
|
||||
|
||||
// Vertically center the text block
|
||||
startY := (CardHeight-totalH)/2 + 48 // baseline of first title line
|
||||
|
||||
// Draw title
|
||||
for i, line := range titleLines {
|
||||
drawText(img, leftPad, startY+i*60, line, faces.title, theme.TitleColor)
|
||||
}
|
||||
|
||||
// Draw description
|
||||
descY := startY + titleBlockH + 16
|
||||
for i, line := range descLines {
|
||||
drawText(img, leftPad, descY+i*34, line, faces.desc, theme.DescColor)
|
||||
}
|
||||
|
||||
// Bottom metadata: language + repo name
|
||||
metaY := descY + descBlockH + gapBeforeMeta
|
||||
xCursor := leftPad
|
||||
if data.LanguageName != "" {
|
||||
langColor := parseHexColor(data.LanguageColor)
|
||||
drawCircle(img, xCursor+8, metaY-6, 8, langColor)
|
||||
xCursor += 24
|
||||
drawText(img, xCursor, metaY, data.LanguageName, faces.meta, theme.SubtextColor)
|
||||
xCursor += measureText(data.LanguageName, faces.meta) + 30
|
||||
}
|
||||
drawText(img, xCursor, metaY, data.RepoFullName, faces.meta, theme.SubtextColor)
|
||||
|
||||
// GitCaddy logo (bottom right)
|
||||
logoBounds := r.logo.Bounds()
|
||||
logoX := CardWidth - rightPad - logoBounds.Dx()
|
||||
logoY := CardHeight - 40 - logoBounds.Dy()
|
||||
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
|
||||
r.logo, logoBounds.Min, draw.Over)
|
||||
|
||||
// Encode to PNG
|
||||
return encodePNG(img)
|
||||
}
|
||||
|
||||
// renderSolidCard renders a card with a user-chosen solid background color
|
||||
// and auto-contrast text colors.
|
||||
// renderSolidCard renders a portrait card with a solid-color top half
|
||||
// containing center-aligned text, and the background image in the bottom half.
|
||||
// If no background image is provided, the solid color fills the entire card.
|
||||
func (r *Renderer) renderSolidCard(data CardData) ([]byte, error) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, CardWidth, CardHeight))
|
||||
w, h := PortraitWidth, PortraitHeight
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
|
||||
bgColor := parseHexColor(data.SolidColor)
|
||||
theme := contrastTheme(bgColor)
|
||||
|
||||
// Fill entire card with solid color first
|
||||
fillRect(img, img.Bounds(), bgColor)
|
||||
|
||||
// Top accent bar — slightly lighter/darker than background
|
||||
accentColor := shiftColor(bgColor, 40)
|
||||
fillRect(img, image.Rect(0, 0, CardWidth, 4), accentColor)
|
||||
// If there's a background image, draw it in the bottom half
|
||||
splitY := h / 2
|
||||
if data.BgImage != nil {
|
||||
bottomH := h - splitY
|
||||
cropped := coverCrop(data.BgImage, w, bottomH)
|
||||
draw.Draw(img,
|
||||
image.Rect(0, splitY, w, h),
|
||||
cropped, cropped.Bounds().Min, draw.Src)
|
||||
}
|
||||
|
||||
faces, err := r.createFaces()
|
||||
if err != nil {
|
||||
@@ -228,56 +300,89 @@ func (r *Renderer) renderSolidCard(data CardData) ([]byte, error) {
|
||||
}
|
||||
defer faces.close()
|
||||
|
||||
padding := 60
|
||||
avatarSize := 96
|
||||
avatarGap := 24
|
||||
pad := 80
|
||||
contentWidth := w - 2*pad
|
||||
|
||||
hasAvatar := false
|
||||
if data.RepoAvatarURL != "" {
|
||||
avatarImg := fetchAndDecodeAvatar(data.RepoAvatarURL, avatarSize)
|
||||
if avatarImg != nil {
|
||||
avatarX := CardWidth - padding - avatarSize
|
||||
avatarY := padding + 20
|
||||
drawRoundedAvatar(img, avatarX, avatarY, avatarSize, 12, avatarImg)
|
||||
hasAvatar = true
|
||||
// Build text block for the top half, center-aligned
|
||||
titleLines := wrapText(data.Title, faces.title, contentWidth)
|
||||
if len(titleLines) > 3 {
|
||||
titleLines = titleLines[:3]
|
||||
titleLines[2] = truncateWithEllipsis(titleLines[2], faces.title, contentWidth)
|
||||
}
|
||||
titleBlockH := len(titleLines) * 60
|
||||
|
||||
var descLines []string
|
||||
descBlockH := 0
|
||||
if data.Description != "" {
|
||||
descLines = wrapText(data.Description, faces.desc, contentWidth)
|
||||
if len(descLines) > 3 {
|
||||
descLines = descLines[:3]
|
||||
descLines[2] = truncateWithEllipsis(descLines[2], faces.desc, contentWidth)
|
||||
}
|
||||
descBlockH = len(descLines)*34 + 20
|
||||
}
|
||||
|
||||
contentWidth := CardWidth - 2*padding
|
||||
if hasAvatar {
|
||||
contentWidth = CardWidth - 2*padding - avatarSize - avatarGap
|
||||
metaH := 28
|
||||
gapBeforeMeta := 20
|
||||
totalH := titleBlockH + descBlockH + gapBeforeMeta + metaH
|
||||
|
||||
// Vertically center in the top half
|
||||
topCenter := splitY / 2
|
||||
startY := topCenter - totalH/2 + 48
|
||||
|
||||
// Draw title (centered)
|
||||
for i, line := range titleLines {
|
||||
lineW := measureText(line, faces.title)
|
||||
x := (w - lineW) / 2
|
||||
drawText(img, x, startY+i*60, line, faces.title, theme.TitleColor)
|
||||
}
|
||||
|
||||
r.drawCardText(img, faces, data, theme, padding, contentWidth)
|
||||
// Draw description (centered)
|
||||
descY := startY + titleBlockH + 20
|
||||
for i, line := range descLines {
|
||||
lineW := measureText(line, faces.desc)
|
||||
x := (w - lineW) / 2
|
||||
drawText(img, x, descY+i*34, line, faces.desc, theme.DescColor)
|
||||
}
|
||||
|
||||
// Repo full name (centered, smaller)
|
||||
metaY := descY + descBlockH + gapBeforeMeta
|
||||
repoW := measureText(data.RepoFullName, faces.meta)
|
||||
drawText(img, (w-repoW)/2, metaY, data.RepoFullName, faces.meta, theme.SubtextColor)
|
||||
|
||||
// GitCaddy logo (bottom right)
|
||||
logoBounds := r.logo.Bounds()
|
||||
logoX := w - pad - logoBounds.Dx()
|
||||
logoY := h - 40 - logoBounds.Dy()
|
||||
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
|
||||
r.logo, logoBounds.Min, draw.Over)
|
||||
|
||||
return encodePNG(img)
|
||||
}
|
||||
|
||||
// renderImageCard renders a card with a full-bleed background image and
|
||||
// a dark gradient scrim at the bottom for text readability.
|
||||
// renderImageCard renders a portrait card with a full-bleed background image,
|
||||
// a strong dark gradient scrim over the bottom ~45%, and white text at the bottom-left.
|
||||
func (r *Renderer) renderImageCard(data CardData) ([]byte, error) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, CardWidth, CardHeight))
|
||||
w, h := PortraitWidth, PortraitHeight
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
|
||||
// Draw background image (cover-crop to fill)
|
||||
if data.BgImage != nil {
|
||||
cropped := coverCrop(data.BgImage, CardWidth, CardHeight)
|
||||
cropped := coverCrop(data.BgImage, w, h)
|
||||
draw.Draw(img, img.Bounds(), cropped, cropped.Bounds().Min, draw.Src)
|
||||
} else {
|
||||
// Fallback to dark if no image
|
||||
fillRect(img, img.Bounds(), ThemeDark.Background)
|
||||
}
|
||||
|
||||
// Draw gradient scrim over bottom 50%
|
||||
scrimStartY := CardHeight * 50 / 100
|
||||
drawGradientScrim(img, scrimStartY, CardHeight, 200)
|
||||
// Strong gradient scrim over bottom 45%
|
||||
scrimStartY := h * 55 / 100
|
||||
drawGradientScrim(img, scrimStartY, h, 220)
|
||||
|
||||
// White text on scrim
|
||||
theme := Theme{
|
||||
TitleColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
DescColor: color.RGBA{R: 220, G: 220, B: 220, A: 255},
|
||||
SubtextColor: color.RGBA{R: 180, G: 180, B: 180, A: 255},
|
||||
AccentColor: color.RGBA{R: 100, G: 160, B: 255, A: 255},
|
||||
}
|
||||
titleColor := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
descColor := color.RGBA{R: 230, G: 230, B: 230, A: 255}
|
||||
subtextColor := color.RGBA{R: 170, G: 170, B: 170, A: 255}
|
||||
|
||||
faces, err := r.createFaces()
|
||||
if err != nil {
|
||||
@@ -285,76 +390,63 @@ func (r *Renderer) renderImageCard(data CardData) ([]byte, error) {
|
||||
}
|
||||
defer faces.close()
|
||||
|
||||
padding := 60
|
||||
|
||||
// Repo avatar top-right (with slight shadow effect for visibility)
|
||||
avatarSize := 80
|
||||
if data.RepoAvatarURL != "" {
|
||||
avatarImg := fetchAndDecodeAvatar(data.RepoAvatarURL, avatarSize)
|
||||
if avatarImg != nil {
|
||||
avatarX := CardWidth - padding - avatarSize
|
||||
avatarY := padding
|
||||
drawRoundedAvatar(img, avatarX, avatarY, avatarSize, 12, avatarImg)
|
||||
}
|
||||
}
|
||||
|
||||
// Text at bottom on scrim — use larger title for impact
|
||||
contentWidth := CardWidth - 2*padding
|
||||
bottomPadding := 60
|
||||
pad := 80
|
||||
contentWidth := w - 2*pad
|
||||
bottomPad := 80
|
||||
|
||||
// Build text from bottom up
|
||||
yBottom := CardHeight - bottomPadding
|
||||
yBottom := h - bottomPad
|
||||
|
||||
// Repo full name (bottom-most)
|
||||
drawText(img, padding, yBottom, data.RepoFullName, faces.meta, theme.SubtextColor)
|
||||
yBottom -= 30
|
||||
// Repo full name (bottom-most, small caps style)
|
||||
drawText(img, pad, yBottom, data.RepoFullName, faces.meta, subtextColor)
|
||||
yBottom -= 36
|
||||
|
||||
// Unsplash attribution
|
||||
// Unsplash attribution (right-aligned on same line as repo name)
|
||||
if data.UnsplashAuthor != "" {
|
||||
attrText := "Photo by " + data.UnsplashAuthor
|
||||
attrX := CardWidth - padding - measureText(attrText, faces.meta)
|
||||
drawText(img, attrX, CardHeight-bottomPadding, attrText, faces.meta, theme.SubtextColor)
|
||||
attrX := w - pad - measureText(attrText, faces.meta)
|
||||
drawText(img, attrX, yBottom+36, attrText, faces.meta, subtextColor)
|
||||
}
|
||||
|
||||
// Language indicator
|
||||
if data.LanguageName != "" {
|
||||
langColor := parseHexColor(data.LanguageColor)
|
||||
drawCircle(img, padding+8, yBottom-6, 8, langColor)
|
||||
drawText(img, padding+24, yBottom, data.LanguageName, faces.meta, theme.SubtextColor)
|
||||
yBottom -= 30
|
||||
drawCircle(img, pad+8, yBottom-6, 8, langColor)
|
||||
drawText(img, pad+24, yBottom, data.LanguageName, faces.meta, subtextColor)
|
||||
yBottom -= 36
|
||||
}
|
||||
|
||||
yBottom -= 10
|
||||
yBottom -= 16
|
||||
|
||||
// Description (max 2 lines for image style)
|
||||
// Description (max 3 lines)
|
||||
if data.Description != "" {
|
||||
descLines := wrapText(data.Description, faces.desc, contentWidth)
|
||||
if len(descLines) > 2 {
|
||||
descLines = descLines[:2]
|
||||
descLines[1] = truncateWithEllipsis(descLines[1], faces.desc, contentWidth)
|
||||
if len(descLines) > 3 {
|
||||
descLines = descLines[:3]
|
||||
descLines[2] = truncateWithEllipsis(descLines[2], faces.desc, contentWidth)
|
||||
}
|
||||
for i := len(descLines) - 1; i >= 0; i-- {
|
||||
drawText(img, padding, yBottom, descLines[i], faces.desc, theme.DescColor)
|
||||
drawText(img, pad, yBottom, descLines[i], faces.desc, descColor)
|
||||
yBottom -= 34
|
||||
}
|
||||
yBottom -= 6
|
||||
yBottom -= 10
|
||||
}
|
||||
|
||||
// Title (max 2 lines)
|
||||
// Title (max 3 lines, large)
|
||||
titleLines := wrapText(data.Title, faces.title, contentWidth)
|
||||
if len(titleLines) > 2 {
|
||||
titleLines = titleLines[:2]
|
||||
titleLines[1] = truncateWithEllipsis(titleLines[1], faces.title, contentWidth)
|
||||
if len(titleLines) > 3 {
|
||||
titleLines = titleLines[:3]
|
||||
titleLines[2] = truncateWithEllipsis(titleLines[2], faces.title, contentWidth)
|
||||
}
|
||||
for i := len(titleLines) - 1; i >= 0; i-- {
|
||||
drawText(img, padding, yBottom, titleLines[i], faces.title, theme.TitleColor)
|
||||
drawText(img, pad, yBottom, titleLines[i], faces.title, titleColor)
|
||||
yBottom -= 60
|
||||
}
|
||||
|
||||
// GitCaddy logo (bottom right)
|
||||
logoBounds := r.logo.Bounds()
|
||||
logoX := CardWidth - padding - logoBounds.Dx()
|
||||
logoY := CardHeight - padding - logoBounds.Dy()
|
||||
logoX := w - pad - logoBounds.Dx()
|
||||
logoY := h - pad + 10 - logoBounds.Dy()
|
||||
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
|
||||
r.logo, logoBounds.Min, draw.Over)
|
||||
|
||||
@@ -399,55 +491,6 @@ func (r *Renderer) createFaces() (*fontFaces, error) {
|
||||
return &fontFaces{title: titleFace, desc: descFace, meta: metaFace}, nil
|
||||
}
|
||||
|
||||
// drawCardText draws title, description, language, repo name, and logo
|
||||
// using the standard layout (used by preset and solid styles).
|
||||
func (r *Renderer) drawCardText(img *image.RGBA, faces *fontFaces, data CardData, theme Theme, padding, contentWidth int) {
|
||||
// Draw title (max 2 lines)
|
||||
titleY := 80 + 48
|
||||
titleLines := wrapText(data.Title, faces.title, contentWidth)
|
||||
if len(titleLines) > 2 {
|
||||
titleLines = titleLines[:2]
|
||||
titleLines[1] = truncateWithEllipsis(titleLines[1], faces.title, contentWidth)
|
||||
}
|
||||
for i, line := range titleLines {
|
||||
drawText(img, padding, titleY+i*60, line, faces.title, theme.TitleColor)
|
||||
}
|
||||
|
||||
// Draw description (max 3 lines)
|
||||
descY := titleY + len(titleLines)*60 + 20
|
||||
if data.Description != "" {
|
||||
descLines := wrapText(data.Description, faces.desc, contentWidth)
|
||||
if len(descLines) > 3 {
|
||||
descLines = descLines[:3]
|
||||
descLines[2] = truncateWithEllipsis(descLines[2], faces.desc, contentWidth)
|
||||
}
|
||||
for i, line := range descLines {
|
||||
drawText(img, padding, descY+i*34, line, faces.desc, theme.DescColor)
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom section: language + repo full name
|
||||
bottomY := CardHeight - 80
|
||||
xCursor := padding
|
||||
|
||||
if data.LanguageName != "" {
|
||||
langColor := parseHexColor(data.LanguageColor)
|
||||
drawCircle(img, xCursor+8, bottomY-6, 8, langColor)
|
||||
xCursor += 24
|
||||
drawText(img, xCursor, bottomY, data.LanguageName, faces.meta, theme.SubtextColor)
|
||||
xCursor += measureText(data.LanguageName, faces.meta) + 30
|
||||
}
|
||||
|
||||
drawText(img, xCursor, bottomY, data.RepoFullName, faces.meta, theme.SubtextColor)
|
||||
|
||||
// GitCaddy logo (bottom right)
|
||||
logoBounds := r.logo.Bounds()
|
||||
logoX := CardWidth - padding - logoBounds.Dx()
|
||||
logoY := CardHeight - padding - logoBounds.Dy()
|
||||
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
|
||||
r.logo, logoBounds.Min, draw.Over)
|
||||
}
|
||||
|
||||
// fillRect fills a rectangle with a solid color.
|
||||
func fillRect(img *image.RGBA, rect image.Rectangle, c color.RGBA) {
|
||||
for y := rect.Min.Y; y < rect.Max.Y; y++ {
|
||||
@@ -662,10 +705,11 @@ func drawGradientScrim(img *image.RGBA, startY, endY int, maxAlpha uint8) {
|
||||
if height <= 0 {
|
||||
return
|
||||
}
|
||||
imgW := img.Bounds().Dx()
|
||||
for y := startY; y < endY; y++ {
|
||||
progress := float64(y-startY) / float64(height)
|
||||
alpha := uint8(float64(maxAlpha) * progress)
|
||||
for x := range CardWidth {
|
||||
for x := range imgW {
|
||||
r, g, b, a := img.At(x, y).RGBA()
|
||||
// Blend with black at the computed alpha
|
||||
blendR := uint8((r>>8)*(255-uint32(alpha))/255 + 0)
|
||||
|
||||
Reference in New Issue
Block a user