Skip to content

Commit c8de8c2

Browse files
authored
Improve canvas baselines and reflowing (#22)
* Restore icon-derived buff colours with readable contrast * Align GCD baselines and label continuing buff tails * Default canvas view to fit to window
1 parent bb1939d commit c8de8c2

18 files changed

Lines changed: 134 additions & 59 deletions

src/app/render-harness/RenderHarness.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ const ogcd = (id: string, name: string, extra: Partial<Action> = {}): Action =>
3838
})
3939

4040
const fixtures: Record<string, { prepullRotation: Action[]; rotation: Action[]; title?: string; expansion?: string; patch?: string; logo?: boolean }> = {
41+
'gcd-baseline': {
42+
prepullRotation: [],
43+
rotation: [gcd('g1', 'Rising Raptor'), gcd('g2', 'Shadow of the Destroyer'), gcd('g3', 'Six-sided Star')],
44+
},
45+
'gcd-lanes': {
46+
prepullRotation: [],
47+
rotation: [gcd('g1', 'Rising Raptor', { recastTime: 0 }), gcd('g2', 'Shadow of the Destroyer', { recastTime: 0 }), gcd('g3', 'Six-sided Star', { recastTime: 0 })],
48+
},
49+
'buff-tail': {
50+
prepullRotation: [],
51+
rotation: [gcd('g1', 'Heated Split Shot'), gcd('g2', 'Heated Slug Shot'), ogcd('o1', 'Wildfire', { statusesApplied: [{ ...buff, name: 'Wildfire', duration: 20, applicationDelay: 0.6 }] })],
52+
},
4153
empty: { prepullRotation: [], rotation: [] },
4254
ordinary: {
4355
prepullRotation: [ogcd('p1', 'Grade 6 Gemdraught of Intelligence', { prepull: -2, statusesApplied: [buff] })],

src/components/Canvas/Canvas.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ const Canvas = forwardRef<HTMLCanvasElement, CanvasProps>((props, ref) => {
135135
})
136136
const [renderStatus, setRenderStatus] = useState<CanvasRenderState['status']>('loading')
137137
const [renderError, setRenderError] = useState('')
138-
// false = scale to pane height (default); true = fit the whole canvas in the pane
139-
const [fitToWindow, setFitToWindow] = useState(false)
138+
// true = fit the whole canvas in the pane (default); false = scale to pane height
139+
const [fitToWindow, setFitToWindow] = useState(true)
140140
const [viewportSize, setViewportSize] = useState({ width: 0, height: 0 })
141141

142142
useImperativeHandle(ref, () => innerRef.current!, [])

src/components/Canvas/calculateBuffLinePositions.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { MutableRefObject } from 'react'
21
import { CanvasBuffLine, CanvasIcon, CanvasoGCD, TimelinePoint } from './types'
32

43
export const calculateTimeline = (
@@ -107,7 +106,8 @@ const interpolateTime = (timeline: TimelinePoint[], x: number) => {
107106
const before = reverseSortedPoints.find(point => point.x <= x)
108107
const after = sortedPoints.find(point => point.x > x)
109108

110-
if (!before || !after) return sortedPoints[0].time
109+
if (!before) return sortedPoints[0].time
110+
if (!after) return before.time + (before.addedWeaveTime ?? 0)
111111

112112
const beforeTime = before.time + (before.addedWeaveTime ?? 0)
113113
const afterTime = after.time + (after.addedWeaveTime ?? 0)
@@ -119,26 +119,30 @@ const interpolateTime = (timeline: TimelinePoint[], x: number) => {
119119
export const calculateBuffLinePositions = (
120120
icons: CanvasIcon[],
121121
timeline: TimelinePoint[],
122-
iconRefs: MutableRefObject<(HTMLImageElement | null)[]>,
123122
finalX: number,
124123
): CanvasBuffLine[] => {
125124
const buffLines: CanvasBuffLine[] = []
126125

127-
icons.forEach((icon, index) => {
126+
icons.forEach(icon => {
128127
if (icon.type !== 'gcd' && icon.type !== 'ogcd') return
129128
;(icon.statusesApplied ?? []).forEach((status, statusIndex) => {
130129
if (status.enabled === false) return
131130
const initialX = icon.x + icon.width
132131
const actionTime = icon.prepull ?? interpolateTime(timeline, initialX)
133132
const startTime = actionTime + (status.applicationDelay ?? 0)
133+
const endTime = startTime + status.duration
134+
const rotationEndTime = timeline.at(-1)?.time ?? 0
135+
if (status.duration <= 0 || startTime > rotationEndTime || endTime < (timeline[0]?.time ?? 0)) return
134136
const startX = interpolateX(timeline, startTime, finalX)
135137

136138
buffLines.push({
137139
instanceKey: `${icon.instanceId}:${statusIndex}`,
138140
status: status,
139-
icon: iconRefs.current[index] ?? null,
141+
startTime,
142+
endTime,
143+
continuesAfter: endTime > rotationEndTime,
140144
startX: startX,
141-
endX: interpolateX(timeline, startTime + status.duration, finalX),
145+
endX: interpolateX(timeline, endTime, finalX),
142146
})
143147
})
144148
})

src/components/Canvas/layoutInfographic.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ describe('infographic layout plan', () => {
101101
const ids = plan.primitives.map(primitive => primitive.id)
102102
expect(ids.filter(id => id.startsWith('buff-') && id.endsWith('-start'))).toHaveLength(1)
103103
expect(ids.filter(id => id.endsWith('-continue-before'))).toHaveLength(2)
104-
expect(ids.filter(id => id.startsWith('buff-') && id.endsWith('-arrow'))).toHaveLength(2)
105-
expect(ids.filter(id => id.startsWith('buff-') && id.endsWith('-end'))).toHaveLength(1)
104+
expect(ids.filter(id => id.startsWith('buff-') && id.endsWith('-arrow'))).toHaveLength(3)
105+
expect(ids.filter(id => id.startsWith('buff-') && id.endsWith('-end'))).toHaveLength(0)
106106
expect(plan.textBlocks.filter(block => block.role === 'buff')).toHaveLength(3)
107107
expect(auditRenderPlan(plan)).toEqual([])
108108
})
@@ -219,6 +219,60 @@ describe('infographic layout plan', () => {
219219
expect(count!.bounds.y).toBeGreaterThan(name!.bounds.y + name!.bounds.height)
220220
})
221221

222+
it.each([1, 2])('anchors counts below upward-growing name lanes (%s rows)', rowCount => {
223+
const plan = layoutInfographic({ ...baseInput, rowCount, rotation: [
224+
action('g1', 'gcd', { name: 'Rising Raptor', recastTime: 0 }),
225+
action('g2', 'gcd', { name: 'Shadow of the Destroyer', recastTime: 0 }),
226+
action('g3', 'gcd', { name: 'Six-sided Star', recastTime: 0 }),
227+
action('g4', 'gcd', { name: '', recastTime: 0 }),
228+
] }, measurer)
229+
const icons = actionIcons(plan)
230+
const counts = plan.textBlocks.filter(block => block.role === 'count')
231+
expect(counts.map(block => block.lines[0].text)).toEqual(['1', '2', '3', '4'])
232+
for (const y of Array.from(new Set(icons.map(icon => icon.bounds.y)))) {
233+
const owners = icons.filter(icon => icon.bounds.y === y).map(icon => icon.ownerId)
234+
const rowCounts = counts.filter(count => owners.includes(count.ownerId))
235+
expect(new Set(rowCounts.map(count => count.lines[0].y)).size).toBe(1)
236+
const names = plan.textBlocks.filter(block => block.role === 'action' && owners.includes(block.ownerId))
237+
expect(names.every(name => name.bounds.y >= y + icons[0].bounds.height)).toBe(true)
238+
expect(names.every(name => name.bounds.y + name.bounds.height < rowCounts[0].bounds.y)).toBe(true)
239+
}
240+
expect(auditRenderPlan(plan)).toEqual([])
241+
})
242+
243+
it.each([1, 2])('distinguishes expiry from continuation and includes short tails in bounds (%s rows)', rowCount => {
244+
const make = (duration: number, applicationDelay = 0) => layoutInfographic({ ...baseInput, rowCount,
245+
rotation: [action('g1'), action('g2', 'gcd', { statusesApplied: [{
246+
id: 'buff', name: 'A continuing status with a long label', imageSrc: '/favicon.ico',
247+
color: '#abcdef', duration, applicationDelay,
248+
}] })],
249+
}, measurer)
250+
for (const duration of [1, 2.5]) {
251+
const plan = make(duration)
252+
expect(plan.primitives.some(item => item.id.endsWith('-end'))).toBe(true)
253+
expect(plan.primitives.some(item => item.id.endsWith('-arrow'))).toBe(false)
254+
}
255+
for (const delay of [0, 2.5]) {
256+
const plan = make(20, delay)
257+
expect(plan.primitives.some(item => item.id.endsWith('-arrow'))).toBe(true)
258+
expect(plan.primitives.some(item => item.id.endsWith('-end'))).toBe(false)
259+
expect(plan.textBlocks.filter(block => block.role === 'buff')).toHaveLength(1)
260+
expect(auditRenderPlan(plan)).toEqual([])
261+
}
262+
expect(make(20, 3).primitives.some(item => item.ownerId?.startsWith('buff-'))).toBe(false)
263+
expect(make(0).primitives.some(item => item.ownerId?.startsWith('buff-'))).toBe(false)
264+
})
265+
266+
it('grows the canvas for a late continuation without stretching the timeline', () => {
267+
const rotation = Array.from({ length: 10 }, (_, i) => action(String(i)))
268+
const original = layoutInfographic({ ...baseInput, rotation }, measurer)
269+
rotation[9].statusesApplied = [{ id: 'tail', name: 'A long continuing buff at the end of the rotation', imageSrc: '/favicon.ico', color: '#abcdef', duration: 20, applicationDelay: 2 }]
270+
const plan = layoutInfographic({ ...baseInput, rotation }, measurer)
271+
expect(plan.width).toBeGreaterThan(original.width)
272+
expect(actionIcons(plan).map(icon => icon.bounds.x)).toEqual(actionIcons(original).map(icon => icon.bounds.x))
273+
expect(auditRenderPlan(plan)).toEqual([])
274+
})
275+
222276
it('adds outward lanes and connectors for consecutive oGCD labels', () => {
223277
const plan = layoutInfographic({
224278
...baseInput,

0 commit comments

Comments
 (0)