Skip to content

Commit 037e232

Browse files
committed
fix: focus stacking diagram updates and translation fixes
1 parent 48ea4bf commit 037e232

11 files changed

Lines changed: 155 additions & 162 deletions

File tree

src/app/[locale]/focus-stacking-calculator/_components/StackingDiagram.tsx

Lines changed: 135 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -10,205 +10,178 @@ interface StackingDiagramProps {
1010
farLimit: number
1111
}
1212

13-
const BAND_COLORS = [
14-
'var(--lens-a, #3b82f6)', // blue
15-
'var(--lens-b, #ef4444)', // red
16-
'var(--lens-c, #22c55e)', // green
17-
'#f59e0b', // amber
18-
'#8b5cf6', // purple
19-
'#ec4899', // pink
13+
const COLORS = [
14+
'#3b82f6', '#ef4444', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899',
2015
]
21-
22-
const PADDING_LEFT = 70
23-
const PADDING_RIGHT = 24
24-
const PADDING_TOP = 32
25-
const PADDING_BOTTOM = 40
26-
const BAND_HEIGHT = 20
27-
const BAND_GAP = 4
28-
const SVG_MIN_HEIGHT = 200
29-
30-
function distToX(d: number, logMin: number, logRange: number, drawWidth: number): number {
31-
return PADDING_LEFT + ((Math.log(d) - logMin) / logRange) * drawWidth
16+
const PAD = { top: 28, right: 24, bottom: 40, left: 56 }
17+
const VB_W = 800
18+
const DRAW_W = VB_W - PAD.left - PAD.right
19+
const BAND_H = 18
20+
const BAND_GAP = 3
21+
const MAX_BAND_SHOTS = 20
22+
const COV_H = 32
23+
const COV_COLS = 200
24+
const FOCUS_H = 28
25+
26+
function distToX(d: number, logMin: number, logRange: number): number {
27+
return PAD.left + ((Math.log(d) - logMin) / logRange) * DRAW_W
3228
}
3329

3430
function generateTicks(min: number, max: number): number[] {
35-
const ticks: number[] = []
36-
const decades = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100]
37-
for (const d of decades) {
38-
if (d >= min * 0.9 && d <= max * 1.1) ticks.push(d)
39-
}
40-
return ticks
31+
return [0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100]
32+
.filter((d) => d >= min * 0.9 && d <= max * 1.1)
33+
}
34+
35+
function fmtDist(d: number): string {
36+
return d >= 1 ? `${d}m` : `${Math.round(d * 100)}cm`
4137
}
4238

4339
export function StackingDiagram({ result, nearLimit, farLimit }: StackingDiagramProps) {
4440
const t = useTranslations('toolUI.focus-stacking-calculator')
4541
const { shots } = result
42+
const showBands = shots.length <= MAX_BAND_SHOTS
4643

47-
const diagramData = useMemo(() => {
48-
// Determine log scale bounds with some padding
49-
const allDistances = shots.flatMap((s) => [s.nearFocus, s.farFocus])
50-
allDistances.push(nearLimit, farLimit)
51-
const minD = Math.max(0.05, Math.min(...allDistances) * 0.8)
52-
const maxD = Math.max(...allDistances) * 1.2
53-
44+
const data = useMemo(() => {
45+
const allD = shots.flatMap((s) => [s.nearFocus, s.farFocus]).concat(nearLimit, farLimit)
46+
const minD = Math.max(0.01, Math.min(...allD) * 0.8)
47+
const maxD = Math.max(...allD) * 1.2
5448
const logMin = Math.log(minD)
55-
const logMax = Math.log(maxD)
56-
const logRange = logMax - logMin
57-
58-
const bandsHeight = shots.length * (BAND_HEIGHT + BAND_GAP)
59-
const svgHeight = Math.max(SVG_MIN_HEIGHT, PADDING_TOP + bandsHeight + PADDING_BOTTOM)
60-
49+
const logRange = Math.log(maxD) - logMin
6150
const ticks = generateTicks(minD, maxD)
6251

63-
return { logMin, logRange, svgHeight, ticks, minD, maxD }
52+
const cov = new Array(COV_COLS).fill(0) as number[]
53+
let maxCov = 0
54+
for (let c = 0; c < COV_COLS; c++) {
55+
const d = Math.exp(logMin + (c / (COV_COLS - 1)) * logRange)
56+
for (const s of shots) {
57+
if (d >= s.nearFocus && d <= s.farFocus) cov[c]++
58+
}
59+
if (cov[c] > maxCov) maxCov = cov[c]
60+
}
61+
return { logMin, logRange, ticks, cov, maxCov }
6462
}, [shots, nearLimit, farLimit])
6563

66-
const { logMin, logRange, svgHeight, ticks } = diagramData
64+
const { logMin, logRange, ticks, cov, maxCov } = data
65+
66+
// Layout vertical sections
67+
let y = PAD.top
68+
const covY = y; y += COV_H + 10
69+
const focusY = y; y += FOCUS_H
70+
let bandsY = 0
71+
if (showBands) { y += 10; bandsY = y; y += shots.length * (BAND_H + BAND_GAP) }
72+
const svgH = y + PAD.bottom
73+
74+
const nearX = distToX(nearLimit, logMin, logRange)
75+
const farX = distToX(farLimit, logMin, logRange)
76+
const colW = DRAW_W / COV_COLS
6777

68-
// We use viewBox for responsiveness; drawWidth is in viewBox units
69-
const viewBoxWidth = 800
70-
const drawWidth = viewBoxWidth - PADDING_LEFT - PADDING_RIGHT
78+
const dotR = shots.length <= 10 ? 4 : shots.length <= 30 ? 3 : shots.length <= 60 ? 2.5 : 2
79+
const step = shots.length <= 40 ? 1 : shots.length <= 80 ? 2 : Math.ceil(shots.length / 40)
7180

72-
const nearX = distToX(nearLimit, logMin, logRange, drawWidth)
73-
const farX = distToX(farLimit, logMin, logRange, drawWidth)
81+
// Deduplicated legend values
82+
const legendVals = [1, Math.ceil(maxCov / 2), maxCov].filter((v, i, a) => a.indexOf(v) === i)
7483

7584
return (
76-
<svg
77-
viewBox={`0 0 ${viewBoxWidth} ${svgHeight}`}
78-
width="100%"
79-
style={{ maxWidth: 900, height: 'auto' }}
80-
role="img"
81-
aria-label={t('diagram')}
82-
>
83-
{/* Background */}
84-
<rect x={0} y={0} width={viewBoxWidth} height={svgHeight} fill="var(--bg-surface)" rx={8} />
85-
86-
{/* Near/far limit dashed lines */}
87-
<line
88-
x1={nearX} y1={PADDING_TOP - 12}
89-
x2={nearX} y2={svgHeight - PADDING_BOTTOM + 8}
90-
stroke="var(--accent)" strokeWidth={1.5} strokeDasharray="4 3" opacity={0.7}
91-
/>
92-
<text
93-
x={nearX} y={PADDING_TOP - 16}
94-
fill="var(--accent)" fontSize={10} textAnchor="middle" fontWeight={600}
95-
>
96-
{t('diagramNear')}
97-
</text>
98-
<line
99-
x1={farX} y1={PADDING_TOP - 12}
100-
x2={farX} y2={svgHeight - PADDING_BOTTOM + 8}
101-
stroke="var(--accent)" strokeWidth={1.5} strokeDasharray="4 3" opacity={0.7}
102-
/>
103-
<text
104-
x={farX} y={PADDING_TOP - 16}
105-
fill="var(--accent)" fontSize={10} textAnchor="middle" fontWeight={600}
106-
>
107-
{t('diagramFar')}
108-
</text>
109-
110-
{/* Camera icon (simple representation) */}
111-
<text
112-
x={PADDING_LEFT - 10} y={PADDING_TOP + (shots.length * (BAND_HEIGHT + BAND_GAP)) / 2 + 4}
113-
fill="var(--text-secondary)" fontSize={20} textAnchor="end"
114-
>
115-
{'\uD83D\uDCF7'}
116-
</text>
117-
118-
{/* Shot bands */}
85+
<svg viewBox={`0 0 ${VB_W} ${svgH}`} width="100%"
86+
style={{ maxWidth: 900, height: 'auto' }} role="img" aria-label={t('diagram')}>
87+
<rect x={0} y={0} width={VB_W} height={svgH} fill="var(--bg-surface)" rx={8} />
88+
89+
{/* Near / far limit lines */}
90+
<line x1={nearX} y1={PAD.top - 10} x2={nearX} y2={svgH - PAD.bottom + 8}
91+
stroke="var(--accent)" strokeWidth={1.5} strokeDasharray="4 3" opacity={0.6} />
92+
<text x={nearX} y={PAD.top - 14} fill="var(--accent)" fontSize={10}
93+
textAnchor="middle" fontWeight={600}>{t('diagramNear')}</text>
94+
<line x1={farX} y1={PAD.top - 10} x2={farX} y2={svgH - PAD.bottom + 8}
95+
stroke="var(--accent)" strokeWidth={1.5} strokeDasharray="4 3" opacity={0.6} />
96+
<text x={farX} y={PAD.top - 14} fill="var(--accent)" fontSize={10}
97+
textAnchor="middle" fontWeight={600}>{t('diagramFar')}</text>
98+
99+
{/* === Coverage heatmap === */}
100+
<text x={PAD.left - 8} y={covY + COV_H / 2 + 3} fill="var(--text-secondary)"
101+
fontSize={9} textAnchor="end" fontWeight={500}>{t('diagramCoverage')}</text>
102+
{cov.map((count, i) => count > 0 ? (
103+
<rect key={i} x={PAD.left + i * colW} y={covY} width={colW + 0.5} height={COV_H}
104+
fill="var(--accent)"
105+
opacity={0.15 + Math.min(1, count / Math.max(2, maxCov * 0.6)) * 0.55} />
106+
) : null)}
107+
<rect x={PAD.left} y={covY} width={DRAW_W} height={COV_H}
108+
fill="none" stroke="var(--border)" strokeWidth={1} rx={4} />
109+
{maxCov > 1 && legendVals.map((count, i) => {
110+
const lx = VB_W - PAD.right - (legendVals.length - 1 - i) * 44 - 10
111+
const op = 0.15 + Math.min(1, count / Math.max(2, maxCov * 0.6)) * 0.55
112+
return (
113+
<g key={count}>
114+
<rect x={lx} y={covY - 14} width={12} height={8}
115+
fill="var(--accent)" opacity={op} rx={2} />
116+
<text x={lx + 16} y={covY - 7} fill="var(--text-secondary)" fontSize={8}>
117+
{count}x
118+
</text>
119+
</g>
120+
)
121+
})}
122+
123+
{/* === Focus point dots === */}
124+
<text x={PAD.left - 8} y={focusY + 14} fill="var(--text-secondary)"
125+
fontSize={9} textAnchor="end" fontWeight={500}>{t('diagramFocusPoints')}</text>
126+
<line x1={PAD.left} y1={focusY + 14} x2={VB_W - PAD.right} y2={focusY + 14}
127+
stroke="var(--border)" strokeWidth={1} />
119128
{shots.map((shot, i) => {
120-
const color = BAND_COLORS[i % BAND_COLORS.length]
121-
const y = PADDING_TOP + i * (BAND_HEIGHT + BAND_GAP)
122-
const x1 = distToX(shot.nearFocus, logMin, logRange, drawWidth)
123-
const x2 = distToX(shot.farFocus, logMin, logRange, drawWidth)
124-
const bandWidth = Math.max(2, x2 - x1)
125-
126-
// Overlap zone with previous shot
127-
let overlapX1 = 0
128-
let overlapWidth = 0
129+
if (i !== 0 && i !== shots.length - 1 && i % step !== 0) return null
130+
return (
131+
<circle key={shot.number} cx={distToX(shot.focusDistance, logMin, logRange)}
132+
cy={focusY + 14} r={dotR} fill={COLORS[i % COLORS.length]} opacity={0.85} />
133+
)
134+
})}
135+
<text x={VB_W - PAD.right + 4} y={focusY + 17}
136+
fill="var(--text-secondary)" fontSize={9}>{shots.length}</text>
137+
138+
{/* === Individual bands (few shots only) === */}
139+
{showBands && shots.map((shot, i) => {
140+
const color = COLORS[i % COLORS.length]
141+
const by = bandsY + i * (BAND_H + BAND_GAP)
142+
const x1 = distToX(shot.nearFocus, logMin, logRange)
143+
const x2 = distToX(shot.farFocus, logMin, logRange)
144+
const bw = Math.max(2, x2 - x1)
145+
const fx = distToX(shot.focusDistance, logMin, logRange)
146+
let olW = 0, olX = 0
129147
if (i > 0) {
130148
const prev = shots[i - 1]
131149
if (prev.farFocus > shot.nearFocus) {
132-
overlapX1 = x1
133-
const overlapX2 = distToX(
134-
Math.min(prev.farFocus, shot.farFocus),
135-
logMin, logRange, drawWidth,
136-
)
137-
overlapWidth = Math.max(0, overlapX2 - overlapX1)
150+
olX = x1
151+
olW = Math.max(0, distToX(
152+
Math.min(prev.farFocus, shot.farFocus), logMin, logRange,
153+
) - x1)
138154
}
139155
}
140-
141156
return (
142157
<g key={shot.number}>
143-
{/* Main band */}
144-
<rect
145-
x={x1} y={y}
146-
width={bandWidth} height={BAND_HEIGHT}
147-
fill={color} opacity={0.35} rx={3}
148-
/>
149-
<rect
150-
x={x1} y={y}
151-
width={bandWidth} height={BAND_HEIGHT}
152-
fill="none" stroke={color} strokeWidth={1.5} rx={3}
153-
/>
154-
155-
{/* Overlap zone highlight */}
156-
{overlapWidth > 0 && (
157-
<rect
158-
x={overlapX1} y={y}
159-
width={overlapWidth} height={BAND_HEIGHT}
160-
fill={color} opacity={0.15} rx={2}
161-
/>
162-
)}
163-
164-
{/* Focus point marker */}
165-
{(() => {
166-
const fx = distToX(shot.focusDistance, logMin, logRange, drawWidth)
167-
return (
168-
<circle cx={fx} cy={y + BAND_HEIGHT / 2} r={3} fill={color} />
169-
)
170-
})()}
171-
172-
{/* Shot number label */}
173-
<text
174-
x={x1 - 6} y={y + BAND_HEIGHT / 2 + 4}
175-
fill="var(--text-secondary)" fontSize={10} textAnchor="end"
176-
fontWeight={500}
177-
>
178-
{shot.number}
179-
</text>
158+
<rect x={x1} y={by} width={bw} height={BAND_H} fill={color} opacity={0.35} rx={3} />
159+
<rect x={x1} y={by} width={bw} height={BAND_H}
160+
fill="none" stroke={color} strokeWidth={1.5} rx={3} />
161+
{olW > 0 && <rect x={olX} y={by} width={olW} height={BAND_H}
162+
fill={color} opacity={0.15} rx={2} />}
163+
<circle cx={fx} cy={by + BAND_H / 2} r={3} fill={color} />
164+
<text x={x1 - 6} y={by + BAND_H / 2 + 4} fill="var(--text-secondary)"
165+
fontSize={10} textAnchor="end" fontWeight={500}>{shot.number}</text>
180166
</g>
181167
)
182168
})}
183169

184-
{/* Distance scale at bottom */}
170+
{/* === Distance axis === */}
185171
{ticks.map((d) => {
186-
const x = distToX(d, logMin, logRange, drawWidth)
187-
if (x < PADDING_LEFT || x > viewBoxWidth - PADDING_RIGHT) return null
188-
const label = d >= 1 ? `${d}m` : `${Math.round(d * 100)}cm`
172+
const x = distToX(d, logMin, logRange)
173+
if (x < PAD.left || x > VB_W - PAD.right) return null
189174
return (
190175
<g key={d}>
191-
<line
192-
x1={x} y1={svgHeight - PADDING_BOTTOM}
193-
x2={x} y2={svgHeight - PADDING_BOTTOM + 6}
194-
stroke="var(--text-secondary)" strokeWidth={1} opacity={0.5}
195-
/>
196-
<text
197-
x={x} y={svgHeight - PADDING_BOTTOM + 18}
198-
fill="var(--text-secondary)" fontSize={10} textAnchor="middle"
199-
>
200-
{label}
201-
</text>
176+
<line x1={x} y1={svgH - PAD.bottom} x2={x} y2={svgH - PAD.bottom + 6}
177+
stroke="var(--text-secondary)" strokeWidth={1} opacity={0.5} />
178+
<text x={x} y={svgH - PAD.bottom + 18} fill="var(--text-secondary)"
179+
fontSize={10} textAnchor="middle">{fmtDist(d)}</text>
202180
</g>
203181
)
204182
})}
205-
206-
{/* Axis line */}
207-
<line
208-
x1={PADDING_LEFT} y1={svgHeight - PADDING_BOTTOM}
209-
x2={viewBoxWidth - PADDING_RIGHT} y2={svgHeight - PADDING_BOTTOM}
210-
stroke="var(--border)" strokeWidth={1}
211-
/>
183+
<line x1={PAD.left} y1={svgH - PAD.bottom} x2={VB_W - PAD.right} y2={svgH - PAD.bottom}
184+
stroke="var(--border)" strokeWidth={1} />
212185
</svg>
213186
)
214187
}

src/lib/i18n/messages/de/tools/focus-stacking-calculator.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"diagram": "Focus Stack Diagram",
2424
"diagramNear": "Near",
2525
"diagramFar": "Far",
26+
"diagramCoverage": "Coverage",
27+
"diagramFocusPoints": "Focus pts",
2628
"diagramShot": "Shot {number}",
2729
"diagramOverlap": "Overlap zone",
2830
"diagramSharpZone": "Sharp zone",

src/lib/i18n/messages/en/tools/focus-stacking-calculator.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"diagram": "Focus Stack Diagram",
2424
"diagramNear": "Near",
2525
"diagramFar": "Far",
26+
"diagramCoverage": "Coverage",
27+
"diagramFocusPoints": "Focus pts",
2628
"diagramShot": "Shot {number}",
2729
"diagramOverlap": "Overlap zone",
2830
"diagramSharpZone": "Sharp zone",

src/lib/i18n/messages/es/tools/focus-stacking-calculator.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"diagram": "Focus Stack Diagram",
2424
"diagramNear": "Near",
2525
"diagramFar": "Far",
26+
"diagramCoverage": "Coverage",
27+
"diagramFocusPoints": "Focus pts",
2628
"diagramShot": "Shot {number}",
2729
"diagramOverlap": "Overlap zone",
2830
"diagramSharpZone": "Sharp zone",

src/lib/i18n/messages/fr/tools/focus-stacking-calculator.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"diagram": "Focus Stack Diagram",
2424
"diagramNear": "Near",
2525
"diagramFar": "Far",
26+
"diagramCoverage": "Coverage",
27+
"diagramFocusPoints": "Focus pts",
2628
"diagramShot": "Shot {number}",
2729
"diagramOverlap": "Overlap zone",
2830
"diagramSharpZone": "Sharp zone",

src/lib/i18n/messages/hi/tools/focus-stacking-calculator.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"diagram": "Focus Stack Diagram",
2424
"diagramNear": "Near",
2525
"diagramFar": "Far",
26+
"diagramCoverage": "Coverage",
27+
"diagramFocusPoints": "Focus pts",
2628
"diagramShot": "Shot {number}",
2729
"diagramOverlap": "Overlap zone",
2830
"diagramSharpZone": "Sharp zone",

src/lib/i18n/messages/it/tools/focus-stacking-calculator.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"diagram": "Focus Stack Diagram",
2424
"diagramNear": "Near",
2525
"diagramFar": "Far",
26+
"diagramCoverage": "Coverage",
27+
"diagramFocusPoints": "Focus pts",
2628
"diagramShot": "Shot {number}",
2729
"diagramOverlap": "Overlap zone",
2830
"diagramSharpZone": "Sharp zone",

0 commit comments

Comments
 (0)