map marker rings keyed on surface, legend parity
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
:key="t.machinetypeid"
|
||||
class="legend-item"
|
||||
>
|
||||
<span class="legend-dot" :style="{ background: getTypeColor(t.machinetype) }"></span>
|
||||
<span class="legend-dot" :style="legendDotStyle(getTypeColor(t.machinetype))"></span>
|
||||
{{ t.machinetype }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
:key="subtypeId"
|
||||
class="legend-item"
|
||||
>
|
||||
<span class="legend-dot" :style="{ background: color }"></span>
|
||||
<span class="legend-dot" :style="legendDotStyle(color)"></span>
|
||||
{{ subtypeNames[subtypeId] || `Type ${subtypeId}` }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -64,7 +64,7 @@
|
||||
:key="assetType"
|
||||
class="legend-item"
|
||||
>
|
||||
<span class="legend-dot" :style="{ background: color }"></span>
|
||||
<span class="legend-dot" :style="legendDotStyle(color)"></span>
|
||||
{{ assetTypeLabels[assetType] || assetType }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -240,6 +240,13 @@ const visibleAssetTypes = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
// Legend swatches wear the same ring as the markers they stand for, so the key
|
||||
// reads as a key. Without this the dot took a surface-colored border and a light
|
||||
// color looked ringed in white next to its black-ringed marker.
|
||||
function legendDotStyle(color) {
|
||||
return { background: color, borderColor: markerRingColor(props.theme) }
|
||||
}
|
||||
|
||||
// Get visible subtypes when a type is selected
|
||||
const visibleSubtypes = computed(() => {
|
||||
if (!props.selectedAssetType) return {}
|
||||
@@ -477,9 +484,9 @@ function renderMarkers() {
|
||||
const marker = L.circleMarker([leafletY, leafletX], {
|
||||
radius: 6,
|
||||
fillColor: color,
|
||||
// Ring picked from the fill, not fixed white - a white ring on a light
|
||||
// marker vanished against the light (white) blueprint.
|
||||
color: markerRingColor(color),
|
||||
// Ring keyed on the theme's surface: dark on the white blueprint, light on
|
||||
// the dark one. A fixed white ring vanished against the light blueprint.
|
||||
color: markerRingColor(props.theme),
|
||||
weight: 2,
|
||||
fillOpacity: 1,
|
||||
renderer: canvasRenderer
|
||||
@@ -630,6 +637,8 @@ watch(() => props.machines, (newVal, oldVal) => {
|
||||
watch(() => props.theme, (newTheme) => {
|
||||
if (imageOverlay && map) {
|
||||
imageOverlay.setUrl(blueprintUrlFor(newTheme))
|
||||
// Marker rings are keyed on the surface, so they have to be redrawn too.
|
||||
renderMarkers()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -53,19 +53,17 @@ export function getSubtypeId(asset) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Ring color for a marker of the given fill. A fixed white ring disappears on
|
||||
// the white blueprint, so pick the ring by the FILL's lightness instead: a light
|
||||
// fill gets a dark ring, a dark fill a light one. The marker then keeps a hard
|
||||
// edge on either surface, and it also rescues a washed-out color a user picked
|
||||
// by hand for a subtype - which the palette below cannot control.
|
||||
export function markerRingColor(fill) {
|
||||
const hex = (fill || '').replace('#', '')
|
||||
if (hex.length !== 6) return '#ffffff'
|
||||
const [r, g, b] = [0, 2, 4].map(i => parseInt(hex.slice(i, i + 2), 16) / 255)
|
||||
// Relative luminance (WCAG), same formula the palette validator uses.
|
||||
const channel = c => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
|
||||
const luminance = 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
|
||||
return luminance > 0.35 ? '#1b1b1b' : '#ffffff'
|
||||
// Ring color for a marker, keyed on the SURFACE it sits on, not on its fill.
|
||||
// The ring's job is to separate the marker from the background, so every marker
|
||||
// in a theme wears the same one - keying it off the fill instead singled out the
|
||||
// light colors with a black ring while their neighbours kept white ones.
|
||||
//
|
||||
// A ring close to its own fill is never a problem: a fill that resembles the
|
||||
// ring is by definition far from the background, which is what carries the
|
||||
// separation. So a dark ring on white and a light ring on the dark card work for
|
||||
// every fill, including a washed-out color a user picked by hand for a subtype.
|
||||
export function markerRingColor(theme) {
|
||||
return theme === 'light' ? '#1b1b1b' : '#ffffff'
|
||||
}
|
||||
|
||||
// Resolve the marker color for an asset the same way ShopFloorMap does: when a
|
||||
|
||||
@@ -58,20 +58,21 @@ describe('marker legibility', () => {
|
||||
expect(contrast(color, DARK_CARD)).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it('rings a light fill dark and a dark fill light', () => {
|
||||
expect(markerRingColor('#FFEB3B')).toBe('#1b1b1b')
|
||||
expect(markerRingColor('#1565c0')).toBe('#ffffff')
|
||||
it('rings by surface, not by fill', () => {
|
||||
// Uniform within a theme: every marker on the white blueprint wears the
|
||||
// dark ring, every marker on the dark one wears the light ring. Keying it
|
||||
// off the fill singled out the light colors with a black ring.
|
||||
expect(markerRingColor('light')).toBe('#1b1b1b')
|
||||
expect(markerRingColor('dark')).toBe('#ffffff')
|
||||
})
|
||||
|
||||
it('gives every ring 3:1 against its own fill', () => {
|
||||
for (const color of palette) {
|
||||
expect(contrast(color, markerRingColor(color))).toBeGreaterThanOrEqual(3)
|
||||
}
|
||||
it('gives each ring 3:1 against the surface it sits on', () => {
|
||||
expect(contrast(markerRingColor('light'), WHITE)).toBeGreaterThanOrEqual(3)
|
||||
expect(contrast(markerRingColor('dark'), DARK_CARD)).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it('falls back to a light ring for a malformed color', () => {
|
||||
expect(markerRingColor('')).toBe('#ffffff')
|
||||
expect(markerRingColor('nonsense')).toBe('#ffffff')
|
||||
it('defaults to the light ring when the theme is missing', () => {
|
||||
expect(markerRingColor(undefined)).toBe('#ffffff')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -97,8 +97,8 @@ export async function exportMapPdf(opts) {
|
||||
if (lx + w > pageW - margin) { lx = margin; ly += lineH }
|
||||
const [r, g, b] = hexToRgb(entry.color)
|
||||
doc.setFillColor(r, g, b)
|
||||
// Outline the swatch against the white page, same rule as the markers.
|
||||
doc.setDrawColor(markerRingColor(entry.color)); doc.setLineWidth(0.4)
|
||||
// The page is always white, so the ring is the light-surface one throughout.
|
||||
doc.setDrawColor(markerRingColor('light')); doc.setLineWidth(0.4)
|
||||
doc.rect(lx, ly - swatch + 1, swatch, swatch, 'FD')
|
||||
doc.setTextColor('#333333')
|
||||
doc.text(label, lx + swatch + 4, ly)
|
||||
@@ -131,8 +131,8 @@ export async function exportMapPdf(opts) {
|
||||
const fill = resolveMarkerColor(a, { selectedType, subtypeColors })
|
||||
const [r, g, b] = hexToRgb(fill)
|
||||
doc.setFillColor(r, g, b)
|
||||
// The PDF prints on white, where a white ring is invisible - ring by fill.
|
||||
doc.setDrawColor(markerRingColor(fill)); doc.setLineWidth(0.6)
|
||||
// Prints on white, where a white ring is invisible - use the dark ring.
|
||||
doc.setDrawColor(markerRingColor('light')); doc.setLineWidth(0.6)
|
||||
doc.circle(x, y, radius, 'FD')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user