refactor: use package-level cropToActiveBeads in generateDayIcon script
Replace custom bounding box calculations with the new cropToActiveBeads prop: - Remove 100+ lines of manual bbox calculation code (getAbacusBoundingBox) - Use AbacusStatic instead of AbacusReact for simpler SSR - Let cropToActiveBeads handle all cropping logic with padding config - Simplify script to just parse dimensions from cropped SVG and center in canvas This eliminates code duplication and leverages the precise cropping utilities now available in the package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ac6c3c1376
commit
b6c3d6bda4
|
|
@ -8,114 +8,15 @@
|
|||
|
||||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import {
|
||||
AbacusReact,
|
||||
numberToAbacusState,
|
||||
calculateStandardDimensions,
|
||||
calculateBeadPosition,
|
||||
type BeadPositionConfig,
|
||||
} from '@soroban/abacus-react'
|
||||
import { AbacusStatic } from '@soroban/abacus-react'
|
||||
|
||||
// Extract just the SVG element content from rendered output
|
||||
function extractSvgContent(markup: string): string {
|
||||
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)
|
||||
// Extract just the SVG element from rendered output
|
||||
function extractSvgElement(markup: string): string {
|
||||
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
|
||||
if (!svgMatch) {
|
||||
throw new Error('No SVG element found in rendered output')
|
||||
}
|
||||
return svgMatch[1]
|
||||
}
|
||||
|
||||
// Calculate bounding box that includes active beads AND structural elements (posts, bar)
|
||||
interface BoundingBox {
|
||||
minX: number
|
||||
minY: number
|
||||
maxX: number
|
||||
maxY: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bounding box for icon cropping using actual bead position calculations
|
||||
* This replaces fragile regex parsing with deterministic position math
|
||||
*/
|
||||
function getAbacusBoundingBox(
|
||||
day: number,
|
||||
scaleFactor: number,
|
||||
columns: number
|
||||
): BoundingBox {
|
||||
// Get which beads are active for this day
|
||||
const abacusState = numberToAbacusState(day, columns)
|
||||
|
||||
// Get layout dimensions
|
||||
const dimensions = calculateStandardDimensions({
|
||||
columns,
|
||||
scaleFactor,
|
||||
showNumbers: false,
|
||||
columnLabels: [],
|
||||
})
|
||||
|
||||
// Calculate positions of all active beads
|
||||
const activeBeadPositions: Array<{ x: number; y: number }> = []
|
||||
|
||||
for (let placeValue = 0; placeValue < columns; placeValue++) {
|
||||
const columnState = abacusState[placeValue]
|
||||
if (!columnState) continue
|
||||
|
||||
// Heaven bead
|
||||
if (columnState.heavenActive) {
|
||||
const bead: BeadPositionConfig = {
|
||||
type: 'heaven',
|
||||
active: true,
|
||||
position: 0,
|
||||
placeValue,
|
||||
}
|
||||
const pos = calculateBeadPosition(bead, dimensions, { earthActive: columnState.earthActive })
|
||||
activeBeadPositions.push(pos)
|
||||
}
|
||||
|
||||
// Earth beads
|
||||
for (let earthPos = 0; earthPos < columnState.earthActive; earthPos++) {
|
||||
const bead: BeadPositionConfig = {
|
||||
type: 'earth',
|
||||
active: true,
|
||||
position: earthPos,
|
||||
placeValue,
|
||||
}
|
||||
const pos = calculateBeadPosition(bead, dimensions, { earthActive: columnState.earthActive })
|
||||
activeBeadPositions.push(pos)
|
||||
}
|
||||
}
|
||||
|
||||
if (activeBeadPositions.length === 0) {
|
||||
// Fallback if no active beads - show full abacus
|
||||
return { minX: 0, minY: 0, maxX: 50 * scaleFactor, maxY: 120 * scaleFactor }
|
||||
}
|
||||
|
||||
// Calculate bounding box from active bead positions
|
||||
const beadSize = dimensions.beadSize
|
||||
const beadWidth = beadSize * 2.5 // Diamond width is ~2.5x the size parameter
|
||||
const beadHeight = beadSize * 1.8 // Diamond height is ~1.8x the size parameter
|
||||
|
||||
let minX = Infinity
|
||||
let maxX = -Infinity
|
||||
let minY = Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
for (const pos of activeBeadPositions) {
|
||||
// Bead center is at pos.x, pos.y
|
||||
// Calculate bounding box for diamond shape
|
||||
minX = Math.min(minX, pos.x - beadWidth / 2)
|
||||
maxX = Math.max(maxX, pos.x + beadWidth / 2)
|
||||
minY = Math.min(minY, pos.y - beadHeight / 2)
|
||||
maxY = Math.max(maxY, pos.y + beadHeight / 2)
|
||||
}
|
||||
|
||||
// HORIZONTAL BOUNDS: Always show full width of all columns (consistent across all days)
|
||||
// Use rod positions for consistent horizontal bounds
|
||||
const rodSpacing = dimensions.rodSpacing
|
||||
minX = rodSpacing / 2 - beadWidth / 2
|
||||
maxX = (columns - 0.5) * rodSpacing + beadWidth / 2
|
||||
|
||||
return { minX, minY, maxX, maxY }
|
||||
return svgMatch[0]
|
||||
}
|
||||
|
||||
// Get day from command line argument
|
||||
|
|
@ -128,15 +29,23 @@ if (!day || day < 1 || day > 31) {
|
|||
}
|
||||
|
||||
// Render 2-column abacus showing day of month
|
||||
// Using AbacusStatic for server-side rendering
|
||||
const abacusMarkup = renderToStaticMarkup(
|
||||
<AbacusReact
|
||||
<AbacusStatic
|
||||
value={day}
|
||||
columns={2}
|
||||
scaleFactor={1.8}
|
||||
animated={false}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
hideInactiveBeads={true}
|
||||
frameVisible={true}
|
||||
cropToActiveBeads={{
|
||||
padding: {
|
||||
top: 8,
|
||||
bottom: 2,
|
||||
left: 5,
|
||||
right: 5,
|
||||
},
|
||||
}}
|
||||
customStyles={{
|
||||
columnPosts: {
|
||||
fill: '#1c1917',
|
||||
|
|
@ -164,44 +73,43 @@ const abacusMarkup = renderToStaticMarkup(
|
|||
/>
|
||||
)
|
||||
|
||||
let svgContent = extractSvgContent(abacusMarkup)
|
||||
// Extract the cropped SVG
|
||||
let croppedSvg = extractSvgElement(abacusMarkup)
|
||||
|
||||
// Remove !important from CSS (production code policy)
|
||||
svgContent = svgContent.replace(/\s*!important/g, '')
|
||||
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
|
||||
|
||||
// Calculate bounding box using proper bead position calculations
|
||||
const bbox = getAbacusBoundingBox(day, 1.8, 2)
|
||||
// Parse width and height from the cropped SVG
|
||||
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
|
||||
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
|
||||
|
||||
// Add minimal padding around active beads (in abacus coordinates)
|
||||
// Less padding below since we want to cut tight to the last bead
|
||||
const paddingTop = 8
|
||||
const paddingBottom = 2
|
||||
const paddingSide = 5
|
||||
const cropX = bbox.minX - paddingSide
|
||||
const cropY = bbox.minY - paddingTop
|
||||
const cropWidth = bbox.maxX - bbox.minX + paddingSide * 2
|
||||
const cropHeight = bbox.maxY - bbox.minY + paddingTop + paddingBottom
|
||||
if (!widthMatch || !heightMatch) {
|
||||
throw new Error('Could not parse dimensions from cropped SVG')
|
||||
}
|
||||
|
||||
const croppedWidth = parseFloat(widthMatch[1])
|
||||
const croppedHeight = parseFloat(heightMatch[1])
|
||||
|
||||
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
|
||||
const targetSize = 96
|
||||
const scale = Math.min(targetSize / cropWidth, targetSize / cropHeight)
|
||||
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
|
||||
|
||||
// Center in 100x100 canvas
|
||||
const scaledWidth = cropWidth * scale
|
||||
const scaledHeight = cropHeight * scale
|
||||
const scaledWidth = croppedWidth * scale
|
||||
const scaledHeight = croppedHeight * scale
|
||||
const offsetX = (100 - scaledWidth) / 2
|
||||
const offsetY = (100 - scaledHeight) / 2
|
||||
|
||||
// Wrap in SVG with proper viewBox for favicon sizing
|
||||
// Use nested SVG with viewBox to actually CROP the content, not just scale it
|
||||
// Wrap in 100x100 SVG canvas for favicon
|
||||
// Extract viewBox from cropped SVG to preserve it
|
||||
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
|
||||
const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`
|
||||
|
||||
const svg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
|
||||
<!-- Nested SVG with viewBox does the actual cropping -->
|
||||
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
|
||||
viewBox="${cropX} ${cropY} ${cropWidth} ${cropHeight}">
|
||||
<g class="hide-inactive-mode">
|
||||
${svgContent}
|
||||
</g>
|
||||
viewBox="${viewBox}">
|
||||
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
|
||||
</svg>
|
||||
</svg>
|
||||
`
|
||||
|
|
|
|||
Loading…
Reference in New Issue