Skip to content

Commit bad15c6

Browse files
committed
fix: Frame Studio grid offset rendering for line-based grids
- Compute exact line positions for rule-of-thirds, golden ratio, center cross, and square grid when offset is applied - Prevents uneven spacing when dragging grid overlay - Export dialog uses same position-aware rendering
1 parent 9293dfb commit bad15c6

2 files changed

Lines changed: 101 additions & 13 deletions

File tree

src/app/[locale]/frame-studio/_components/ExportDialog.tsx

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { transferExif } from '@/lib/utils/exif'
77
import {
88
drawRuleOfThirds, drawGoldenRatio, drawGoldenSpiral, drawGoldenDiagonal,
99
drawDiagonalLines, drawCenterCross, drawSquareGrid, drawTriangles,
10+
GOLDEN_RATIO,
1011
} from '@/lib/math/grid'
1112
import type { FrameConfig, CropState, GridType, GridOptions } from './types'
1213
import { thicknessToPx } from './types'
@@ -38,8 +39,30 @@ const GRID_DRAW_MAP: Record<GridType, DrawFn> = {
3839
'triangles': (ctx, w, h) => drawTriangles(ctx, w, h),
3940
}
4041

41-
function drawGridTiled(
42-
ctx: CanvasRenderingContext2D, drawFn: DrawFn,
42+
function getLinePositions(
43+
gridType: GridType, w: number, h: number, opts: GridOptions,
44+
): { v: number[]; h: number[] } | null {
45+
switch (gridType) {
46+
case 'rule-of-thirds':
47+
return { v: [0, w / 3, 2 * w / 3], h: [0, h / 3, 2 * h / 3] }
48+
case 'golden-ratio': {
49+
const pw = w / GOLDEN_RATIO, ph = h / GOLDEN_RATIO
50+
return { v: [0, w - pw, pw], h: [0, h - ph, ph] }
51+
}
52+
case 'center-cross':
53+
return { v: [0, w / 2], h: [0, h / 2] }
54+
case 'square-grid':
55+
return {
56+
v: Array.from({ length: opts.gridDensity }, (_, i) => (i * w) / opts.gridDensity),
57+
h: Array.from({ length: opts.gridDensity }, (_, i) => (i * h) / opts.gridDensity),
58+
}
59+
default:
60+
return null
61+
}
62+
}
63+
64+
function drawExportGrid(
65+
ctx: CanvasRenderingContext2D, gridType: GridType, drawFn: DrawFn,
4366
originX: number, originY: number, gw: number, gh: number,
4467
ox: number, oy: number, opts: GridOptions,
4568
) {
@@ -51,15 +74,30 @@ function drawGridTiled(
5174
ctx.restore()
5275
return
5376
}
54-
const px = ((ox % gw) + gw) % gw
55-
const py = ((oy % gh) + gh) % gh
56-
for (let dx = -1; dx <= 0; dx++) {
57-
for (let dy = -1; dy <= 0; dy++) {
58-
ctx.save()
59-
ctx.translate(originX + px + dx * gw, originY + py + dy * gh)
60-
ctx.beginPath()
61-
drawFn(ctx, gw, gh, opts)
62-
ctx.restore()
77+
78+
const positions = getLinePositions(gridType, gw, gh, opts)
79+
if (positions) {
80+
ctx.beginPath()
81+
for (const base of positions.v) {
82+
const x = ((base + ox) % gw + gw) % gw
83+
if (x > 0.5) { ctx.moveTo(originX + x, originY); ctx.lineTo(originX + x, originY + gh) }
84+
}
85+
for (const base of positions.h) {
86+
const y = ((base + oy) % gh + gh) % gh
87+
if (y > 0.5) { ctx.moveTo(originX, originY + y); ctx.lineTo(originX + gw, originY + y) }
88+
}
89+
ctx.stroke()
90+
} else {
91+
const px = ((ox % gw) + gw) % gw
92+
const py = ((oy % gh) + gh) % gh
93+
for (let dx = -1; dx <= 0; dx++) {
94+
for (let dy = -1; dy <= 0; dy++) {
95+
ctx.save()
96+
ctx.translate(originX + px + dx * gw, originY + py + dy * gh)
97+
ctx.beginPath()
98+
drawFn(ctx, gw, gh, opts)
99+
ctx.restore()
100+
}
63101
}
64102
}
65103
}
@@ -137,7 +175,7 @@ export function ExportDialog({
137175
ctx.lineWidth = thicknessToPx(gridOptions.thickness)
138176
for (const gridType of activeGrids) {
139177
const drawFn = GRID_DRAW_MAP[gridType]
140-
if (drawFn) drawGridTiled(ctx, drawFn, imgX, imgY, sw, sh, ox, oy, gridOptions)
178+
if (drawFn) drawExportGrid(ctx, gridType, drawFn, imgX, imgY, sw, sh, ox, oy, gridOptions)
141179
}
142180
ctx.restore()
143181
}

src/app/[locale]/frame-studio/_components/GridCanvas.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useRef, useEffect, useCallback } from 'react'
44
import {
55
drawRuleOfThirds, drawGoldenRatio, drawGoldenSpiral, drawGoldenDiagonal,
66
drawDiagonalLines, drawCenterCross, drawSquareGrid, drawTriangles,
7+
GOLDEN_RATIO,
78
} from '@/lib/math/grid'
89
import type { GridType, GridOptions } from './types'
910
import { thicknessToPx } from './types'
@@ -29,6 +30,48 @@ const GRID_DRAW_MAP: Record<GridType, DrawFn> = {
2930
'triangles': (ctx, w, h) => drawTriangles(ctx, w, h),
3031
}
3132

33+
/** For H/V line grids, returns all line positions (including a boundary at 0) */
34+
function getLinePositions(
35+
gridType: GridType, w: number, h: number, opts: GridOptions,
36+
): { v: number[]; h: number[] } | null {
37+
switch (gridType) {
38+
case 'rule-of-thirds':
39+
return { v: [0, w / 3, 2 * w / 3], h: [0, h / 3, 2 * h / 3] }
40+
case 'golden-ratio': {
41+
const pw = w / GOLDEN_RATIO, ph = h / GOLDEN_RATIO
42+
return { v: [0, w - pw, pw], h: [0, h - ph, ph] }
43+
}
44+
case 'center-cross':
45+
return { v: [0, w / 2], h: [0, h / 2] }
46+
case 'square-grid':
47+
return {
48+
v: Array.from({ length: opts.gridDensity }, (_, i) => (i * w) / opts.gridDensity),
49+
h: Array.from({ length: opts.gridDensity }, (_, i) => (i * h) / opts.gridDensity),
50+
}
51+
default:
52+
return null
53+
}
54+
}
55+
56+
/** Draw H/V lines at computed positions, correctly wrapping with offset */
57+
function drawOffsetLineGrid(
58+
ctx: CanvasRenderingContext2D,
59+
w: number, h: number,
60+
positions: { v: number[]; h: number[] },
61+
ox: number, oy: number,
62+
) {
63+
ctx.beginPath()
64+
for (const base of positions.v) {
65+
const x = ((base + ox) % w + w) % w
66+
if (x > 0.5) { ctx.moveTo(x, 0); ctx.lineTo(x, h) }
67+
}
68+
for (const base of positions.h) {
69+
const y = ((base + oy) % h + h) % h
70+
if (y > 0.5) { ctx.moveTo(0, y); ctx.lineTo(w, y) }
71+
}
72+
ctx.stroke()
73+
}
74+
3275
export function GridCanvas({ width, height, activeGrids, options, offset }: GridCanvasProps) {
3376
const canvasRef = useRef<HTMLCanvasElement>(null)
3477

@@ -61,8 +104,15 @@ export function GridCanvas({ width, height, activeGrids, options, offset }: Grid
61104
if (ox === 0 && oy === 0) {
62105
ctx.beginPath()
63106
drawFn(ctx, width, height, options)
107+
continue
108+
}
109+
110+
// For H/V line grids, compute exact positions to ensure even spacing
111+
const positions = getLinePositions(gridType, width, height, options)
112+
if (positions) {
113+
drawOffsetLineGrid(ctx, width, height, positions, ox, oy)
64114
} else {
65-
// Tile 2×2 with modular wrapping so the pattern always fills the canvas
115+
// Tile 2×2 for non-line patterns (diagonals, spiral, triangles)
66116
const px = ((ox % width) + width) % width
67117
const py = ((oy % height) + height) % height
68118
for (let dx = -1; dx <= 0; dx++) {

0 commit comments

Comments
 (0)