map: ring markers by surface so a theme rings every marker alike
Keying the ring off the fill singled out the light colors: on the map the orange network-device marker took a black ring while its neighbours kept white ones, and the legend dot for the same type wore a surface-colored border, so the key did not match the markers. Ring by the SURFACE instead - dark on the white blueprint, light on the dark one - which is uniform within a theme and still works for every fill, since a fill that resembles the ring is by definition far from the background. Legend swatches take the same ring, the theme watcher redraws the markers (their ring now depends on it), and the PDF uses the light-surface ring throughout because it prints on white.
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