Skip to content

Commit

Permalink
refactor: crop circle in SVG
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber committed Nov 6, 2024
1 parent 06cb478 commit 5d0cfef
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 29 deletions.
31 changes: 11 additions & 20 deletions src/processing/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,42 +46,33 @@ export async function resolveAvatars(
})

if (pngBuffer) {
const radius = ship.sponsor.type === 'Organization' ? 0.1 : 0.5

// Store the highest resolution version we use of the original image
ship.sponsor.avatarBuffer = await round(pngBuffer, radius, 120)
ship.sponsor.avatarBuffer = await resizeImage(pngBuffer, 120)
}
})))
}

const cache = new Map<string, Map<Buffer, Buffer>>()
export async function round(image: Buffer, radius = 0.5, size = 100) {
const cacheKey = `${radius}:${size}`
if (cache.has(cacheKey)) {
const cacheHit = cache.get(cacheKey)!.get(image)
const cache = new Map<Buffer, Map<number, Buffer>>()
export async function resizeImage(
image: Buffer,
size = 100,
) {
if (cache.has(image)) {
const cacheHit = cache.get(image)!.get(size)
if (cacheHit) {
return cacheHit
}
}

const rect = Buffer.from(
`<svg><rect x="0" y="0" width="${size}" height="${size}" rx="${size * radius}" ry="${size * radius}"/></svg>`,
)

const result = await sharp(image)
.resize(size, size, { fit: sharp.fit.cover })
.composite([{
blend: 'dest-in',
input: rect,
density: 72,
}])
.png({ quality: 80, compressionLevel: 8 })
.toBuffer()

if (!cache.has(cacheKey)) {
cache.set(cacheKey, new Map())
if (!cache.has(image)) {
cache.set(image, new Map())
}
cache.get(cacheKey)!.set(image, result)
cache.get(image)!.set(size, result)

return result
}
Expand Down
29 changes: 20 additions & 9 deletions src/processing/svg.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { round } from './image'
import { resizeImage } from './image'
import type { BadgePreset, Sponsor, SponsorkitRenderOptions, Sponsorship } from '../types'

const dataImagePngBase64 = `data:image/png;base64,`

export function genSvgImage(x: number, y: number, size: number, base64Image: string) {
return `<image x="${x}" y="${y}" width="${size}" height="${size}" href="${dataImagePngBase64}${base64Image}"/>`
let id = 0
export function genSvgImage(
x: number,
y: number,
size: number,
base64Image: string,
imageFormat: 'png' | 'webp',
radius: number,
) {
const cropId = `c${id++}`
return `
<clipPath id="${cropId}">
<rect x="${x}" y="${y}" width="${size}" height="${size}" rx="${size * radius}" ry="${size * radius}" />
</clipPath>
<image x="${x}" y="${y}" width="${size}" height="${size}" href="data:image/${imageFormat};base64,${base64Image}" clip-path="url(#${cropId})"/>`
}

export async function generateBadge(
Expand All @@ -28,13 +39,13 @@ export async function generateBadge(

let avatar
if (size < 50) {
avatar = await round(sponsor.avatarBuffer!, radius, 50)
avatar = await resizeImage(sponsor.avatarBuffer!, 50)
}
else if (size < 90) {
avatar = await round(sponsor.avatarBuffer!, radius, 80)
avatar = await resizeImage(sponsor.avatarBuffer!, 80)
}
else {
avatar = await round(sponsor.avatarBuffer!, radius, 120)
avatar = await resizeImage(sponsor.avatarBuffer!, 120)
}

avatar = avatar.toString('base64')
Expand All @@ -43,7 +54,7 @@ export async function generateBadge(
${preset.name
? `<text x="${x + size / 2}" y="${y + size + 18}" text-anchor="middle" class="${preset.name.classes || 'sponsorkit-name'}" fill="${preset.name.color || 'currentColor'}">${encodeHtmlEntities(name)}</text>
`
: ''}${genSvgImage(x, y, size, avatar)}
: ''}${genSvgImage(x, y, size, avatar, 'webp', radius)}
</a>`.trim()
}

Expand Down

0 comments on commit 5d0cfef

Please sign in to comment.