diff --git a/shieldlib/src/screen_gfx.ts b/shieldlib/src/screen_gfx.ts index eec3087e8..3be40e4be 100644 --- a/shieldlib/src/screen_gfx.ts +++ b/shieldlib/src/screen_gfx.ts @@ -5,7 +5,9 @@ import rgba from "color-rgba"; const defaultFontFamily = '"sans-serif-condensed", "Arial Narrow", sans-serif'; export const shieldFont = (size: number, fontFamily: string) => `condensed 500 ${size}px ${fontFamily || defaultFontFamily}`; -export const fontSizeThreshold = 12; + +//If a computed shield font size is below this value, choose a wider shield if possible +export const fontSizeThreshold = 11.8; // Replaces `sourceVal` with a blend of `lightenVal` and `darkenVal` proportional to the brightness; // i.e. white becomes `darkenVal`, black becomes `lightenVal`, and anit-aliased pixels remain anit-aliased diff --git a/shieldlib/src/shield.js b/shieldlib/src/shield.js index e521455eb..8a8202cde 100644 --- a/shieldlib/src/shield.js +++ b/shieldlib/src/shield.js @@ -76,18 +76,6 @@ function getDrawFunc(shieldDef) { return ShieldDraw.blank; } -function drawShield(r, ctx, shieldDef, routeDef) { - let bannerCount = getBannerCount(shieldDef); - let yOffset = bannerCount * r.px(r.options.bannerHeight); - - //Shift canvas to draw shield below banner - ctx.save(); - ctx.translate(0, yOffset); - let drawFunc = getDrawFunc(shieldDef); - drawFunc(r, ctx, routeDef.ref); - ctx.restore(); -} - function getDrawHeight(r, shieldDef) { if (typeof shieldDef.shapeBlank != "undefined") { return ShieldDraw.shapeHeight(r, shieldDef.shapeBlank.drawFunc); @@ -95,30 +83,7 @@ function getDrawHeight(r, shieldDef) { return r.shieldSize(); } -function drawShieldText(r, ctx, shieldDef, routeDef) { - var bannerCount = getBannerCount(shieldDef); - var shieldBounds = null; - - var shieldArtwork = getRasterShieldBlank(r, shieldDef, routeDef); - let yOffset = bannerCount * r.px(r.options.bannerHeight); - - if (shieldArtwork == null) { - ctx.translate(0, yOffset); - let drawFunc = getDrawFunc(shieldDef); - drawFunc(r, ctx, routeDef.ref); - ctx.translate(0, -yOffset); - - shieldBounds = { - width: ctx.canvas.width, - height: getDrawHeight(r, shieldDef), - }; - } else { - shieldBounds = { - width: shieldArtwork.data.width, - height: shieldArtwork.data.height, - }; - } - +function drawShieldText(r, ctx, shieldDef, routeDef, shieldBounds) { if (shieldDef.notext) { //If the shield definition says not to draw a ref, ignore ref return ctx; @@ -132,8 +97,6 @@ function drawShieldText(r, ctx, shieldDef, routeDef) { shieldBounds ); - textLayout.yBaseline += bannerCount * r.px(r.options.bannerHeight); - if (typeof r.options.SHIELD_TEXT_HALO_COLOR_OVERRIDE !== "undefined") { ctx.strokeStyle = options.SHIELD_TEXT_HALO_COLOR_OVERRIDE; ShieldText.drawShieldHaloText(r, ctx, routeDef.ref, textLayout); @@ -150,8 +113,7 @@ function drawShieldText(r, ctx, shieldDef, routeDef) { ctx.lineWidth = r.px(1); ctx.strokeRect( r.px(shieldDef.padding.left - 0.5), - bannerCount * r.px(r.options.bannerHeight) + - r.px(shieldDef.padding.top - 0.5), + r.px(shieldDef.padding.top - 0.5), shieldBounds.width - r.px(shieldDef.padding.left + shieldDef.padding.right - 1), shieldBounds.height - @@ -299,6 +261,17 @@ function getDrawnShieldBounds(r, shieldDef, ref) { return { width, height }; } +function bannerAreaHeight(r, bannerCount) { + if (bannerCount === 0) { + return 0; + } + return ( + bannerCount * r.px(r.options.bannerHeight) + + //No padding after last banner + (bannerCount - 1) * r.px(r.options.bannerPadding) + ); +} + export function generateShieldCtx(r, routeDef) { let shieldDef = getShieldDef(r.shieldDef, routeDef); @@ -316,18 +289,25 @@ export function generateShieldCtx(r, routeDef) { let width = r.shieldSize(); let height = r.shieldSize(); + let shieldBounds = null; + if (sourceSprite == null) { if (typeof shieldDef.shapeBlank != "undefined") { let bounds = getDrawnShieldBounds(r, shieldDef, routeDef.ref); width = bounds.width; height = bounds.height; } + shieldBounds = { + width: width, + height: getDrawHeight(r, shieldDef), + }; } else { width = sourceSprite.data.width; height = sourceSprite.data.height; + shieldBounds = { width, height }; } - let bannerHeight = bannerCount * r.px(r.options.bannerHeight); + let bannerHeight = bannerAreaHeight(r, bannerCount); height += bannerHeight; //Generate empty canvas sized to the graphic @@ -343,9 +323,15 @@ export function generateShieldCtx(r, routeDef) { // Add the halo around modifier plaque text drawBannerHalos(r, ctx, shieldDef); + //Shift canvas to draw shield below banner + ctx.save(); + ctx.translate(0, bannerHeight); + if (sourceSprite == null) { - drawShield(r, ctx, shieldDef, routeDef); + let drawFunc = getDrawFunc(shieldDef); + drawFunc(r, ctx, routeDef.ref); } else { + //This is a raw copy, so the yOffset (bannerHeight) is needed Gfx.transposeImageData( ctx, sourceSprite, @@ -357,7 +343,9 @@ export function generateShieldCtx(r, routeDef) { } // Draw the shield text - drawShieldText(r, ctx, shieldDef, routeDef); + drawShieldText(r, ctx, shieldDef, routeDef, shieldBounds); + + ctx.restore(); // Add modifier plaque text drawBanners(r, ctx, shieldDef); diff --git a/shieldlib/src/shield_banner.ts b/shieldlib/src/shield_banner.ts index 6961865b5..deb86599d 100644 --- a/shieldlib/src/shield_banner.ts +++ b/shieldlib/src/shield_banner.ts @@ -133,7 +133,7 @@ function drawBannerTextComponent( textComponent: boolean ): void { const bannerPadding = { - top: r.options.bannerPadding, + top: 0, bottom: 0, left: 0, right: 0, @@ -161,7 +161,7 @@ function drawBannerTextComponent( text, textLayout.xBaseline, textLayout.yBaseline + - bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + bannerIndex * r.px(r.options.bannerHeight + r.options.bannerPadding) ); } else { ctx.shadowBlur = 0; @@ -170,7 +170,7 @@ function drawBannerTextComponent( text, textLayout.xBaseline, textLayout.yBaseline + - bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + bannerIndex * r.px(r.options.bannerHeight + r.options.bannerPadding) ); ctx.shadowColor = null; diff --git a/shieldlib/src/shield_text.ts b/shieldlib/src/shield_text.ts index 350f7f02c..5bc1071d0 100644 --- a/shieldlib/src/shield_text.ts +++ b/shieldlib/src/shield_text.ts @@ -137,6 +137,15 @@ function triangleDownTextConstraint( }; } +// Warning!!! Hack!!! +function isRunningInWebKit(): boolean { + if (typeof window === "undefined") { + return false; + } + const userAgent = window.navigator.userAgent; + return /WebKit/i.test(userAgent) && !/Chrome/i.test(userAgent); +} + /** * Determines the position and font size to draw text so that it fits within * a bounding box. @@ -157,34 +166,49 @@ export function layoutShieldText( textLayoutDef: TextLayout, maxFontSize: number = 14 ): TextPlacement { - var padTop = r.px(padding.top) || 0; - var padBot = r.px(padding.bottom) || 0; - var padLeft = r.px(padding.left) || 0; - var padRight = r.px(padding.right) || 0; + let padTop = r.px(padding.top) || 0; + let padBot = r.px(padding.bottom) || 0; + let padLeft = r.px(padding.left) || 0; + let padRight = r.px(padding.right) || 0; - var maxFont = r.px(maxFontSize); + let maxFont = r.px(maxFontSize); //Temporary canvas for text measurment - var ctx = r.gfxFactory.createGraphics(bounds); + let ctx: CanvasRenderingContext2D = r.gfxFactory.createGraphics(bounds); ctx.font = Gfx.shieldFont(Gfx.fontSizeThreshold, r.options.shieldFont); - ctx.textAlign = "center"; + ctx.textAlign = "left"; ctx.textBaseline = "top"; - var metrics = ctx.measureText(text); + let metrics: TextMetrics = ctx.measureText(text); - var textWidth = metrics.width; - var textHeight = metrics.actualBoundingBoxDescent; + let textWidth: number = + Math.abs(metrics.actualBoundingBoxLeft) + + Math.abs(metrics.actualBoundingBoxRight); + let textHeight: number = + Math.abs(metrics.actualBoundingBoxDescent) + + Math.abs(metrics.actualBoundingBoxAscent); - var availHeight = bounds.height - padTop - padBot; - var availWidth = bounds.width - padLeft - padRight; + //Adjust for excess descender text height across browsers + textHeight *= 0.9; - var xBaseline = padLeft + availWidth / 2; + //Adjust for excess text height measured in Webkit engine specifically + if (isRunningInWebKit()) { + textHeight *= 0.54; + } + + let availHeight: number = bounds.height - padTop - padBot; + let availWidth: number = bounds.width - padLeft - padRight; + + let xBaseline: number = padLeft + availWidth / 2; let textLayoutFunc = drawTextFunctions[textLayoutDef.constraintFunc]; - let textConstraint = textLayoutFunc( - { height: availHeight, width: availWidth }, - { height: textHeight, width: textWidth }, + let spaceAvail: Dimension = { height: availHeight, width: availWidth }; + let measuredTextBounds: Dimension = { height: textHeight, width: textWidth }; + + let textConstraint: TextTransform = textLayoutFunc( + spaceAvail, + measuredTextBounds, textLayoutDef.options ); @@ -195,20 +219,23 @@ export function layoutShieldText( ); ctx.font = Gfx.shieldFont(fontSize, r.options.shieldFont); - ctx.textAlign = "center"; + ctx.textAlign = "left"; ctx.textBaseline = "top"; metrics = ctx.measureText(text); - textHeight = metrics.actualBoundingBoxDescent; + textHeight = + Math.abs(metrics.actualBoundingBoxDescent) + + Math.abs(metrics.actualBoundingBoxAscent); let yBaseline: number; switch (textConstraint.valign) { case VerticalAlignment.Top: - yBaseline = padTop; + yBaseline = padTop + metrics.actualBoundingBoxAscent; break; case VerticalAlignment.Bottom: - yBaseline = padTop + availHeight - textHeight; + yBaseline = + padTop + availHeight - textHeight + metrics.actualBoundingBoxAscent; break; case VerticalAlignment.Middle: default: diff --git a/src/js/shield_defs.js b/src/js/shield_defs.js index a906aeb39..275f6157f 100644 --- a/src/js/shield_defs.js +++ b/src/js/shield_defs.js @@ -3320,6 +3320,7 @@ export function loadShields() { shields["omt-ie-national"] = roundedRectShield( Color.shields.green, + Color.shields.white, Color.shields.yellow ); @@ -3535,6 +3536,7 @@ export function loadShields() { shields["omt-gb-trunk"] = roundedRectShield( Color.shields.green, + Color.shields.white, Color.shields.yellow );