diff --git a/shieldlib/README.md b/shieldlib/README.md index 1a85ff789..988dcd3db 100644 --- a/shieldlib/README.md +++ b/shieldlib/README.md @@ -136,6 +136,8 @@ You should create one definition entry for each network. The entry key must matc } }, "banners": ["ALT"], + "bannerTextColor": "#000", + "bannerTextHaloColor": "#FFF", "textLayout": { "constraintFunc": "roundedRect", "options": { @@ -173,6 +175,8 @@ You should create one definition entry for each network. The entry key must matc ![Bannered routes near Downington, PA](https://wiki.openstreetmap.org/w/images/f/f8/Downington_bannered_routes_Americana.png) +- **`bannerTextColor`**: specify the color of the banner text. +- **`bannerTextHaloColor`**: specify the color of the banner knockout halo. - **`textLayout`**: specify how text should be inscribed within the padded bounds of the shield. The text will be drawn at the maximum size allowed by this constraint. See the [text layout functions](#text-layout-functions) section for text layout options. - **`colorLighten`**: specify that the shield artwork should be lightened (multiplied) by the specified color. This means that black areas will be recolor with this color and white areas will remain the same. Alpha values will remain unmodified. - **`colorDarken`**: specify that the shield artwork should be darkened by the specified color. This means that white areas will be recolor with this color and black areas will remain the same. Alpha values will remain unmodified. diff --git a/shieldlib/src/custom_shields.ts b/shieldlib/src/custom_shields.ts index 8eccf845b..dcf7da765 100644 --- a/shieldlib/src/custom_shields.ts +++ b/shieldlib/src/custom_shields.ts @@ -9,7 +9,7 @@ export function paBelt( r: ShieldRenderingContext, ctx: CanvasRenderingContext2D, params: ShapeBlankParams -) { +): number { ShieldDraw.roundedRectangle(r, ctx, { fillColor: "white", strokeColor: "black", @@ -38,7 +38,7 @@ export function paBelt( ctx.lineWidth = lineWidth; ctx.stroke(); - return ctx; + return 20; } // Special case for Branson color-coded routes @@ -46,7 +46,7 @@ export function bransonRoute( r: ShieldRenderingContext, ctx: CanvasRenderingContext2D, params: ShapeBlankParams -) { +): number { ShieldDraw.roundedRectangle(r, ctx, { fillColor: "#006747", strokeColor: "white", @@ -73,7 +73,7 @@ export function bransonRoute( ctx.lineWidth = lineWidth; ctx.stroke(); - return ctx; + return 20; } export function loadCustomShields() { diff --git a/shieldlib/src/shield.js b/shieldlib/src/shield.js index 6594d4a95..dd15c53d0 100644 --- a/shieldlib/src/shield.js +++ b/shieldlib/src/shield.js @@ -1,20 +1,13 @@ "use strict"; -import * as ShieldText from "./shield_text.mjs"; -import * as ShieldDraw from "./shield_canvas_draw"; +import * as ShieldText from "./shield_text.js"; +import * as ShieldDraw from "./shield_canvas_draw.js"; import * as Gfx from "./screen_gfx.js"; - -function drawBannerPart(r, ctx, shieldDef, drawFunc) { - if (shieldDef == null || typeof shieldDef.banners == "undefined") { - return ctx; //Unadorned shield - } - - for (var i = 0; i < shieldDef.banners.length; i++) { - drawFunc(r, ctx, shieldDef.banners[i], i); - } - - return ctx; -} +import { + drawBanners, + drawBannerHalos, + getBannerCount, +} from "./shield_banner.js"; function compoundShieldSize(r, dimension, bannerCount) { return { @@ -29,19 +22,6 @@ export function isValidRef(ref) { return ref !== null && ref.length !== 0 && ref.length <= 6; } -/** - * Get the number of banner placards associated with this shield - * - * @param {*} shield - Shield definition - * @returns the number of banner placards that need to be drawn - */ -function getBannerCount(shield) { - if (shield == null || typeof shield.banners == "undefined") { - return 0; //Unadorned shield - } - return shield.banners.length; -} - /** * Retrieve the shield blank that goes with a particular route. If there are * multiple shields for a route (different widths), it picks the best shield. @@ -359,7 +339,7 @@ export function generateShieldCtx(r, routeDef) { } // Add the halo around modifier plaque text - drawBannerPart(r, ctx, shieldDef, ShieldText.drawBannerHaloText); + drawBannerHalos(r, ctx, shieldDef); if (sourceSprite == null) { drawShield(r, ctx, shieldDef, routeDef); @@ -378,7 +358,7 @@ export function generateShieldCtx(r, routeDef) { drawShieldText(r, ctx, shieldDef, routeDef); // Add modifier plaque text - drawBannerPart(r, ctx, shieldDef, ShieldText.drawBannerText); + drawBanners(r, ctx, shieldDef); return ctx; } diff --git a/shieldlib/src/shield_banner.ts b/shieldlib/src/shield_banner.ts new file mode 100644 index 000000000..6961865b5 --- /dev/null +++ b/shieldlib/src/shield_banner.ts @@ -0,0 +1,179 @@ +import { shieldFont } from "./screen_gfx"; +import { ShieldRenderingContext } from "./shield_renderer"; +import { TextPlacement, layoutShieldText } from "./shield_text"; +import { Dimension, ShieldDefinition, TextLayout } from "./types"; + +let bannerLayout: TextLayout = { + constraintFunc: "rect", +}; + +/** + * Add modifier plaque text + * + * @param r - Shield rendering context + * @param ctx - Canvas drawing context + * @param shieldDef - Shield definition + */ +export function drawBanners( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + shieldDef: ShieldDefinition +) { + if (shieldDef.bannerTextColor) { + ctx.fillStyle = shieldDef.bannerTextColor; + } else { + ctx.fillStyle = r.options.bannerTextColor; + } + drawBannerPart(r, ctx, shieldDef, drawBannerText); +} + +/** + * Add the halo around modifier plaque text + * + * @param r - Shield rendering context + * @param ctx - Canvas drawing context + * @param shieldDef - Shield definition + */ +export function drawBannerHalos( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + shieldDef: ShieldDefinition +) { + if (shieldDef.bannerTextHaloColor) { + ctx.strokeStyle = ctx.shadowColor = shieldDef.bannerTextHaloColor; + } else { + ctx.strokeStyle = ctx.shadowColor = r.options.bannerTextHaloColor; + } + drawBannerPart(r, ctx, shieldDef, drawBannerHaloText); +} + +type BannerDrawComponentFunction = ( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + bannerIndex: number +) => void; + +function drawBannerPart( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + shieldDef: ShieldDefinition, + drawFunc: BannerDrawComponentFunction +): void { + if (shieldDef == null || typeof shieldDef.banners == "undefined") { + return; //Unadorned shield + } + + for (var i = 0; i < shieldDef.banners.length; i++) { + drawFunc(r, ctx, shieldDef.banners[i], i); + } +} + +/** + * Get the number of banner placards associated with this shield + * + * @param shield - Shield definition + * @returns the number of banner placards that need to be drawn + */ +export function getBannerCount(shield: ShieldDefinition): number { + if (shield == null || typeof shield.banners == "undefined") { + return 0; //Unadorned shield + } + return shield.banners.length; +} + +/** + * Draw text on a modifier plate above a shield + * + * @param {*} r - rendering context + * @param {*} ctx - graphics context to draw to + * @param {*} text - text to draw + * @param {*} bannerIndex - plate position to draw, 0=top, incrementing + */ +export function drawBannerText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + bannerIndex: number +): void { + drawBannerTextComponent(r, ctx, text, bannerIndex, true); +} + +/** + * Draw drop shadow for text on a modifier plate above a shield + * + * @param {*} r - rendering context + * @param {*} ctx - graphics context to draw to + * @param {*} text - text to draw + * @param {*} bannerIndex - plate position to draw, 0=top, incrementing + */ +export function drawBannerHaloText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + bannerIndex: number +): void { + drawBannerTextComponent(r, ctx, text, bannerIndex, false); +} + +/** + * Banners are composed of two components: text on top, and a shadow beneath. + * + * @param {*} r - rendering context + * @param {*} ctx - graphics context to draw to + * @param {*} text - text to draw + * @param {*} bannerIndex - plate position to draw, 0=top, incrementing + * @param {*} textComponent - if true, draw the text. If false, draw the halo + */ +function drawBannerTextComponent( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + bannerIndex: number, + textComponent: boolean +): void { + const bannerPadding = { + top: r.options.bannerPadding, + bottom: 0, + left: 0, + right: 0, + }; + + let bannerBounds: Dimension = { + width: ctx.canvas.width, + height: r.px(r.options.bannerHeight - r.options.bannerPadding), + }; + + let textLayout: TextPlacement = layoutShieldText( + r, + text, + bannerPadding, + bannerBounds, + bannerLayout + ); + + ctx.font = shieldFont(textLayout.fontPx, r.options.shieldFont); + ctx.textBaseline = "top"; + ctx.textAlign = "center"; + + if (textComponent) { + ctx.fillText( + text, + textLayout.xBaseline, + textLayout.yBaseline + + bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + ); + } else { + ctx.shadowBlur = 0; + ctx.lineWidth = r.px(2); + ctx.strokeText( + text, + textLayout.xBaseline, + textLayout.yBaseline + + bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + ); + + ctx.shadowColor = null; + ctx.shadowBlur = null; + } +} diff --git a/shieldlib/src/shield_canvas_draw.ts b/shieldlib/src/shield_canvas_draw.ts index 6cfa8154f..d79160fc5 100644 --- a/shieldlib/src/shield_canvas_draw.ts +++ b/shieldlib/src/shield_canvas_draw.ts @@ -4,7 +4,7 @@ * Shield blanks which are drawn rather built from raster shield blanks */ -import * as ShieldText from "./shield_text.mjs"; +import * as ShieldText from "./shield_text"; import { loadCustomShields } from "./custom_shields"; import { ShapeDrawFunction, ShieldRenderingContext } from "./shield_renderer"; import { ShapeBlankParams } from "./types"; @@ -65,7 +65,7 @@ function ellipse( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -116,7 +116,7 @@ export function roundedRectangle( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref?: string -) { +): number { let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -163,7 +163,7 @@ function escutcheon( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let yOffset = params.yOffset == undefined ? 0 : params.yOffset; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -220,7 +220,7 @@ function fishhead( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let pointUp = params.pointUp == undefined ? false : params.pointUp; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -270,6 +270,8 @@ function fishhead( ctx.strokeStyle = outline; ctx.stroke(); } + + return width; } function triangle( @@ -277,7 +279,7 @@ function triangle( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let pointUp = params.pointUp == undefined ? false : params.pointUp; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -343,7 +345,7 @@ function trapezoid( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let shortSideUp = params.shortSideUp == undefined ? false : params.shortSideUp; let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; @@ -405,7 +407,7 @@ function diamond( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -474,7 +476,7 @@ function pentagon( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let pointUp = params.pointUp == undefined ? true : params.pointUp; let yOffset = params.yOffset == undefined ? 0 : params.yOffset; let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; @@ -547,7 +549,7 @@ function hexagonVertical( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let yOffset = params.yOffset == undefined ? 0 : params.yOffset; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -601,17 +603,17 @@ function hexagonHorizontal( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { - let angle = params.sideAngle == undefined ? 0 : params.sideAngle; +): number { + let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; let outlineWidth = params.outlineWidth == undefined ? 1 : params.outlineWidth; - let sine = Math.sin(angle); - let cosine = Math.cos(angle); - let tangent = Math.tan(angle); - let halfComplementTangent = Math.tan(Math.PI / 4 - angle / 2); + let sine = Math.sin(sideAngle); + let cosine = Math.cos(sideAngle); + let tangent = Math.tan(sideAngle); + let halfComplementTangent = Math.tan(Math.PI / 4 - sideAngle / 2); let width = computeWidth(r, params, ref, "hexagonHorizontal"); @@ -667,7 +669,7 @@ function octagonVertical( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { let yOffset = params.yOffset == undefined ? 0 : params.yOffset; let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; let fill = params.fillColor == undefined ? "white" : params.fillColor; @@ -764,7 +766,7 @@ export function draw( ctx: CanvasRenderingContext2D, params: ShapeBlankParams, ref: string -) { +): number { return drawFunctions[name](r, ctx, params, ref); } diff --git a/shieldlib/src/shield_helper.d.ts b/shieldlib/src/shield_helper.d.ts index 85c942bee..177bf9a22 100644 --- a/shieldlib/src/shield_helper.d.ts +++ b/shieldlib/src/shield_helper.d.ts @@ -141,5 +141,6 @@ export declare function pillShield( export function banneredShield( baseDef: ShieldDefinition, - banners: string[] + banners: string[], + bannerColor?: string ): ShieldDefinition; diff --git a/shieldlib/src/shield_helper.ts b/shieldlib/src/shield_helper.ts index 64b141e59..f70c8b0b4 100644 --- a/shieldlib/src/shield_helper.ts +++ b/shieldlib/src/shield_helper.ts @@ -702,10 +702,12 @@ export function pillShield( */ export function banneredShield( baseDef: ShieldDefinition, - banners: string[] + banners: string[], + bannerColor?: string ): ShieldDefinition { return { banners, + bannerTextColor: bannerColor, ...baseDef, }; } diff --git a/shieldlib/src/shield_renderer.ts b/shieldlib/src/shield_renderer.ts index 570dc3aa6..9194feb32 100644 --- a/shieldlib/src/shield_renderer.ts +++ b/shieldlib/src/shield_renderer.ts @@ -48,8 +48,8 @@ export type ShapeDrawFunction = ( r: ShieldRenderingContext, ctx: CanvasRenderingContext2D, params: ShapeBlankParams, - ref: string -) => void; + ref?: string +) => number; class MaplibreGLSpriteRepository implements SpriteRepository { map: Map; diff --git a/shieldlib/src/shield_text.mjs b/shieldlib/src/shield_text.mjs deleted file mode 100644 index 964146c2d..000000000 --- a/shieldlib/src/shield_text.mjs +++ /dev/null @@ -1,362 +0,0 @@ -"use strict"; - -import * as Gfx from "./screen_gfx.js"; - -const VerticalAlignment = { - Middle: "middle", - Top: "top", - Bottom: "bottom", -}; - -function ellipseScale(spaceBounds, textBounds) { - //Math derived from https://mathworld.wolfram.com/Ellipse-LineIntersection.html - var a = spaceBounds.width; - var b = spaceBounds.height; - - var x0 = textBounds.width; - var y0 = textBounds.height; - - return (a * b) / Math.sqrt(a * a * y0 * y0 + b * b * x0 * x0); -} - -function ellipseTextConstraint(spaceBounds, textBounds) { - return { - scale: ellipseScale(spaceBounds, textBounds), - valign: VerticalAlignment.Middle, - }; -} - -function southHalfEllipseTextConstraint(spaceBounds, textBounds) { - return { - scale: ellipseScale(spaceBounds, { - //Turn ellipse 90 degrees - height: textBounds.width / 2, - width: textBounds.height, - }), - valign: VerticalAlignment.Top, - }; -} - -function rectTextConstraint(spaceBounds, textBounds) { - var scaleHeight = spaceBounds.height / textBounds.height; - var scaleWidth = spaceBounds.width / textBounds.width; - - return { - scale: Math.min(scaleWidth, scaleHeight), - valign: VerticalAlignment.Middle, - }; -} - -function roundedRectTextConstraint(spaceBounds, textBounds, options) { - //Shrink space bounds so that corners hit the arcs - let constraintRadius = 2; - if (options !== undefined && options.radius !== undefined) { - constraintRadius = options.radius; - } - - return rectTextConstraint( - { - width: spaceBounds.width - constraintRadius * (2 - Math.sqrt(2)), - height: spaceBounds.height - constraintRadius * (2 - Math.sqrt(2)), - }, - textBounds - ); -} - -function diamondTextConstraint(spaceBounds, textBounds) { - let a = spaceBounds.width; - let b = spaceBounds.height; - - let x0 = textBounds.width; - let y0 = textBounds.height; - - return { - scale: (a * b) / (b * x0 + a * y0), - valign: VerticalAlignment.Middle, - }; -} - -function triangleDownTextConstraint(spaceBounds, textBounds) { - return { - scale: diamondTextConstraint(spaceBounds, textBounds).scale, - valign: VerticalAlignment.Top, - }; -} - -/** - * Determines the position and font size to draw text so that it fits within - * a bounding box. - * - * @param {*} r - rendering context - * @param {*} text - text to draw - * @param {*} padding - top/bottom/left/right padding around text - * @param {*} bounds - size of the overall graphics area - * @param {*} textLayoutDef - algorithm definition for text scaling - * @param {*} maxFontSize - maximum font size - * @returns JOSN object containing (X,Y) draw position and font size - */ -function layoutShieldText( - r, - text, - padding, - bounds, - textLayoutDef, - maxFontSize -) { - 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; - - var maxFont = r.px(maxFontSize); - //Temporary canvas for text measurment - var ctx = r.gfxFactory.createGraphics(bounds); - - ctx.font = Gfx.shieldFont(Gfx.fontSizeThreshold, r.options.shieldFont); - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - - var metrics = ctx.measureText(text); - - var textWidth = metrics.width; - var textHeight = metrics.actualBoundingBoxDescent; - - var availHeight = bounds.height - padTop - padBot; - var availWidth = bounds.width - padLeft - padRight; - - var xBaseline = padLeft + availWidth / 2; - - let textLayoutFunc = drawTextFunctions[textLayoutDef.constraintFunc]; - - let textConstraint = textLayoutFunc( - { height: availHeight, width: availWidth }, - { height: textHeight, width: textWidth }, - textLayoutDef.options - ); - - //If size-to-fill shield text is too big, shrink it - var fontSize = Math.min( - maxFont, - Gfx.fontSizeThreshold * textConstraint.scale - ); - - ctx.font = Gfx.shieldFont(fontSize, r.options.shieldFont); - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - - metrics = ctx.measureText(text); - textHeight = metrics.actualBoundingBoxDescent; - - var yBaseline; - - switch (textConstraint.valign) { - case VerticalAlignment.Top: - yBaseline = padTop; - break; - case VerticalAlignment.Bottom: - yBaseline = padTop + availHeight - textHeight; - break; - case VerticalAlignment.Middle: - default: - yBaseline = padTop + (availHeight - textHeight) / 2; - break; - } - - return { - xBaseline: xBaseline, - yBaseline: yBaseline, - fontPx: fontSize, - }; -} - -const defaultDefForLayout = { - padding: { - top: 0, - bottom: 0, - left: 0, - right: 0, - }, -}; - -/** - * Determines the position and font size to draw text so that it fits within - * a bounding box. - * - * @param {*} r - rendering context - * @param {*} text - text to draw - * @param {*} def - shield definition - * @param {*} bounds - size of the overall graphics area - * @returns JOSN object containing (X,Y) draw position and font size - */ -export function layoutShieldTextFromDef(r, text, def, bounds) { - //FIX - if (def == null) { - def = defaultDefForLayout; - } - - var padding = def.padding || {}; - - var textLayoutDef = { - constraintFunc: "rect", - }; - - var maxFontSize = 14; // default max size - - if (typeof def.textLayout != "undefined") { - textLayoutDef = def.textLayout; - } - - if (typeof def.maxFontSize != "undefined") { - maxFontSize = Math.min(maxFontSize, def.maxFontSize); // shield definition cannot set max size higher than default - } - - return layoutShieldText(r, text, padding, bounds, textLayoutDef, maxFontSize); -} - -/** - * Draw text on a shield - * - * @param {*} r - rendering context - * @param {*} ctx - graphics context to draw to - * @param {*} text - text to draw - * @param {*} textLayout - location to draw text - */ -export function renderShieldText(r, ctx, text, textLayout) { - //Text color is set by fillStyle - configureShieldText(r, ctx, textLayout); - - ctx.fillText(text, textLayout.xBaseline, textLayout.yBaseline); -} - -/** - * Draw drop shadow for text on a shield - * - * @param {*} r - rendering context - * @param {*} ctx - graphics context to draw to - * @param {*} text - text to draw - * @param {*} textLayout - location to draw text - */ -export function drawShieldHaloText(r, ctx, text, textLayout) { - //Stroke color is set by strokeStyle - configureShieldText(r, ctx, textLayout); - - ctx.shadowColor = ctx.strokeStyle; - ctx.shadowBlur = 0; - ctx.lineWidth = r.px(2); - - ctx.strokeText(text, textLayout.xBaseline, textLayout.yBaseline); - ctx.shadowColor = null; - ctx.shadowBlur = null; -} - -function configureShieldText(r, ctx, textLayout) { - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - ctx.font = Gfx.shieldFont(textLayout.fontPx, r.options.shieldFont); -} - -/** - * Draw text on a modifier plate above a shield - * - * @param {*} r - rendering context - * @param {*} ctx - graphics context to draw to - * @param {*} text - text to draw - * @param {*} bannerIndex - plate position to draw, 0=top, incrementing - */ -export function drawBannerText(r, ctx, text, bannerIndex) { - drawBannerTextComponent(r, ctx, text, bannerIndex, true); -} - -/** - * Draw drop shadow for text on a modifier plate above a shield - * - * @param {*} r - rendering context - * @param {*} ctx - graphics context to draw to - * @param {*} text - text to draw - * @param {*} bannerIndex - plate position to draw, 0=top, incrementing - */ -export function drawBannerHaloText(r, ctx, text, bannerIndex) { - drawBannerTextComponent(r, ctx, text, bannerIndex, false); -} - -/** - * Banners are composed of two components: text on top, and a shadow beneath. - * - * @param {*} r - rendering context - * @param {*} ctx - graphics context to draw to - * @param {*} text - text to draw - * @param {*} bannerIndex - plate position to draw, 0=top, incrementing - * @param {*} textComponent - if true, draw the text. If false, draw the halo - */ -function drawBannerTextComponent(r, ctx, text, bannerIndex, textComponent) { - const bannerPadding = { - padding: { - top: r.options.bannerPadding, - bottom: 0, - left: 0, - right: 0, - }, - }; - var textLayout = layoutShieldTextFromDef(r, text, bannerPadding, { - width: ctx.canvas.width, - height: r.px(r.options.bannerHeight - r.options.bannerPadding), - }); - - ctx.font = Gfx.shieldFont(textLayout.fontPx, r.options.shieldFont); - ctx.textBaseline = "top"; - ctx.textAlign = "center"; - - if (textComponent) { - ctx.fillStyle = r.options.bannerTextColor; - ctx.fillText( - text, - textLayout.xBaseline, - textLayout.yBaseline + - bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) - ); - } else { - ctx.strokeStyle = ctx.shadowColor = r.options.bannerTextHaloColor; - ctx.shadowBlur = 0; - ctx.lineWidth = r.px(2); - ctx.strokeText( - text, - textLayout.xBaseline, - textLayout.yBaseline + - bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) - ); - - ctx.shadowColor = null; - ctx.shadowBlur = null; - } -} - -export function calculateTextWidth(r, text, fontSize) { - var ctx = r.emptySprite(); //dummy canvas - ctx.font = Gfx.shieldFont(fontSize, r.options.shieldFont); - return Math.ceil(ctx.measureText(text).width); -} - -export function drawText(name, options, ref) { - return drawTextFunctions[name](options, ref); -} - -//Register text draw functions -const drawTextFunctions = {}; - -/** - * Invoked by a style to implement a custom draw function - * - * @param {*} name name of the function as referenced by the shield definition - * @param {*} fxn callback to the implementing function. Takes two parameters, ref and options - */ -function registerDrawTextFunction(name, fxn) { - drawTextFunctions[name] = fxn; -} - -//Built-in draw functions (standard shapes) -registerDrawTextFunction("diamond", diamondTextConstraint); -registerDrawTextFunction("ellipse", ellipseTextConstraint); -registerDrawTextFunction("rect", rectTextConstraint); -registerDrawTextFunction("roundedRect", roundedRectTextConstraint); -registerDrawTextFunction("southHalfEllipse", southHalfEllipseTextConstraint); -registerDrawTextFunction("triangleDown", triangleDownTextConstraint); diff --git a/shieldlib/src/shield_text.ts b/shieldlib/src/shield_text.ts index e9dc755ef..350f7f02c 100644 --- a/shieldlib/src/shield_text.ts +++ b/shieldlib/src/shield_text.ts @@ -4,6 +4,7 @@ import * as Gfx from "./screen_gfx.js"; import { ShieldRenderingContext } from "./shield_renderer.js"; import { BoxPadding, + Dimension, ShieldDefinition, TextLayout, TextLayoutParameters, @@ -18,11 +19,6 @@ const VerticalAlignment = { type VerticalAlignmentType = (typeof VerticalAlignment)[keyof typeof VerticalAlignment]; -interface Dimension { - width: number; - height: number; -} - type TextLayoutScaler = ( availSize: Dimension, textSize: Dimension, @@ -34,7 +30,7 @@ interface TextTransform { valign: VerticalAlignmentType; } -interface TextPlacement { +export interface TextPlacement { xBaseline: number; yBaseline: number; fontPx: number; @@ -47,10 +43,6 @@ let noPadding: BoxPadding = { right: 0, }; -let bannerLayout: TextLayout = { - constraintFunc: "rectangle", -}; - function ellipseScale(spaceBounds: Dimension, textBounds: Dimension): number { //Math derived from https://mathworld.wolfram.com/Ellipse-LineIntersection.html var a = spaceBounds.width; @@ -157,7 +149,7 @@ function triangleDownTextConstraint( * @param {*} maxFontSize - maximum font size * @returns JOSN object containing (X,Y) draw position and font size */ -function layoutShieldText( +export function layoutShieldText( r: ShieldRenderingContext, text: string, padding: BoxPadding, @@ -239,7 +231,7 @@ const defaultDefForLayout: ShieldDefinition = { right: 0, }, shapeBlank: { - drawFunc: "rectangle", + drawFunc: "rect", params: { fillColor: "white", strokeColor: "black", @@ -343,104 +335,6 @@ function configureShieldText( ctx.font = Gfx.shieldFont(textLayout.fontPx, r.options.shieldFont); } -/** - * Draw text on a modifier plate above a shield - * - * @param {*} r - rendering context - * @param {*} ctx - graphics context to draw to - * @param {*} text - text to draw - * @param {*} bannerIndex - plate position to draw, 0=top, incrementing - */ -export function drawBannerText( - r: ShieldRenderingContext, - ctx: CanvasRenderingContext2D, - text: string, - bannerIndex: number -): void { - drawBannerTextComponent(r, ctx, text, bannerIndex, true); -} - -/** - * Draw drop shadow for text on a modifier plate above a shield - * - * @param {*} r - rendering context - * @param {*} ctx - graphics context to draw to - * @param {*} text - text to draw - * @param {*} bannerIndex - plate position to draw, 0=top, incrementing - */ -export function drawBannerHaloText( - r: ShieldRenderingContext, - ctx: CanvasRenderingContext2D, - text: string, - bannerIndex: number -): void { - drawBannerTextComponent(r, ctx, text, bannerIndex, false); -} - -/** - * Banners are composed of two components: text on top, and a shadow beneath. - * - * @param {*} r - rendering context - * @param {*} ctx - graphics context to draw to - * @param {*} text - text to draw - * @param {*} bannerIndex - plate position to draw, 0=top, incrementing - * @param {*} textComponent - if true, draw the text. If false, draw the halo - */ -function drawBannerTextComponent( - r: ShieldRenderingContext, - ctx: CanvasRenderingContext2D, - text: string, - bannerIndex: number, - textComponent: boolean -): void { - const bannerPadding = { - top: r.options.bannerPadding, - bottom: 0, - left: 0, - right: 0, - }; - - let bannerBounds: Dimension = { - width: ctx.canvas.width, - height: r.px(r.options.bannerHeight - r.options.bannerPadding), - }; - - let textLayout: TextPlacement = layoutShieldText( - r, - text, - bannerPadding, - bannerBounds, - bannerLayout - ); - - ctx.font = Gfx.shieldFont(textLayout.fontPx, r.options.shieldFont); - ctx.textBaseline = "top"; - ctx.textAlign = "center"; - - if (textComponent) { - ctx.fillStyle = r.options.bannerTextColor; - ctx.fillText( - text, - textLayout.xBaseline, - textLayout.yBaseline + - bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) - ); - } else { - ctx.strokeStyle = ctx.shadowColor = r.options.bannerTextHaloColor; - ctx.shadowBlur = 0; - ctx.lineWidth = r.px(2); - ctx.strokeText( - text, - textLayout.xBaseline, - textLayout.yBaseline + - bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) - ); - - ctx.shadowColor = null; - ctx.shadowBlur = null; - } -} - export function calculateTextWidth( r: ShieldRenderingContext, text: string, diff --git a/shieldlib/src/types.d.ts b/shieldlib/src/types.d.ts index 363863df1..a81c725ad 100644 --- a/shieldlib/src/types.d.ts +++ b/shieldlib/src/types.d.ts @@ -56,3 +56,8 @@ export interface GraphicsFactory { */ pixelRatio(): number; } + +export interface Dimension { + width: number; + height: number; +} diff --git a/shieldlib/src/types.ts b/shieldlib/src/types.ts index 2c1038dbb..63ae4ff1d 100644 --- a/shieldlib/src/types.ts +++ b/shieldlib/src/types.ts @@ -19,6 +19,10 @@ export type Exclusive = export interface ShieldDefinitionBase { /** Color of text drawn on a shield */ textColor?: string; + /** Color of banner text */ + bannerTextColor?: string; + /** Color of banner text halo */ + bannerTextHaloColor?: string; /** Padding around shield text */ padding?: BoxPadding; /** Algorithm for expanding text to fill a shield background */ @@ -218,3 +222,8 @@ export interface GraphicsFactory { */ pixelRatio(): number; } + +export interface Dimension { + width: number; + height: number; +} diff --git a/shieldlib/tsconfig.json b/shieldlib/tsconfig.json index db5212aa2..1204f7fe6 100644 --- a/shieldlib/tsconfig.json +++ b/shieldlib/tsconfig.json @@ -11,5 +11,5 @@ "experimentalSpecifierResolution": "node" }, "exclude": ["node_modules", "**/*.json"], - "include": ["src/**/*.ts", "scripts/**.ts"] + "include": ["src/**/*.ts", "scripts/**.ts", "src/shield.js", "src/shield.js"] } diff --git a/src/js/shield_defs.js b/src/js/shield_defs.js index 54b34f5e8..c1085579c 100644 --- a/src/js/shield_defs.js +++ b/src/js/shield_defs.js @@ -509,7 +509,8 @@ export function loadShields() { textColor: Color.shields.brown, colorLighten: Color.shields.brown, }, - ["HIST"] + ["HIST"], + Color.shields.brown ); // Federal Agencies @@ -553,7 +554,11 @@ export function loadShields() { notext: true, }; - shields["GLCT:Loop"] = banneredShield(shields["GLCT"], ["LOOP"]); + shields["GLCT:Loop"] = banneredShield( + shields["GLCT"], + ["LOOP"], + Color.shields.brown + ); // Alaska shields["US:AK"] = { @@ -765,7 +770,11 @@ export function loadShields() { bottom: 4, }, }; - shields["US:CA:Business"] = banneredShield(shields["US:CA"], ["BUS"]); + shields["US:CA:Business"] = banneredShield( + shields["US:CA"], + ["BUS"], + Color.shields.green + ); shields["US:CA:CR"] = pentagonUpShield( 3, 15, @@ -1170,7 +1179,8 @@ export function loadShields() { textColor: Color.shields.green, colorLighten: Color.shields.green, }, - ["BUS"] + ["BUS"], + Color.shields.green ); // Maine @@ -1570,9 +1580,11 @@ export function loadShields() { spriteBlank: "shield_us_nj_ace_noref", notext: true, }; - shields["US:NJ:ACE:Connector"] = banneredShield(shields["US:NJ:ACE"], [ - "CONN", - ]); + shields["US:NJ:ACE:Connector"] = banneredShield( + shields["US:NJ:ACE"], + ["CONN"], + Color.shields.blue + ); shields["US:NJ:GSP"] = { spriteBlank: "shield_us_nj_gsp_noref", notext: true, @@ -1617,8 +1629,16 @@ export function loadShields() { Color.shields.white, Color.shields.black ); - shields["US:NJ:CR:Spur"] = banneredShield(shields["US:NJ:CR"], ["SPUR"]); - shields["US:NJ:CR:Truck"] = banneredShield(shields["US:NJ:CR"], ["TRK"]); + shields["US:NJ:CR:Spur"] = banneredShield( + shields["US:NJ:CR"], + ["SPUR"], + Color.shields.blue + ); + shields["US:NJ:CR:Truck"] = banneredShield( + shields["US:NJ:CR"], + ["TRK"], + Color.shields.blue + ); // New Mexico shields["US:NM"] = pillShield( @@ -2126,9 +2146,21 @@ export function loadShields() { bottom: 3, }, }; - shields["US:SC:Truck"] = banneredShield(shields["US:SC"], ["TRK"]); - shields["US:SC:Business"] = banneredShield(shields["US:SC"], ["BUS"]); - shields["US:SC:Alternate"] = banneredShield(shields["US:SC"], ["ALT"]); + shields["US:SC:Truck"] = banneredShield( + shields["US:SC"], + ["TRK"], + Color.shields.blue + ); + shields["US:SC:Business"] = banneredShield( + shields["US:SC"], + ["BUS"], + Color.shields.blue + ); + shields["US:SC:Alternate"] = banneredShield( + shields["US:SC"], + ["ALT"], + Color.shields.blue + ); // South Dakota shields["US:SD"] = { @@ -2265,7 +2297,8 @@ export function loadShields() { textColor: Color.shields.brown, colorLighten: Color.shields.brown, }, - ["R"] + ["R"], + Color.shields.brown ); shields["US:TX:NASA"] = banneredShield(shields["US:TX"], ["NASA"]); @@ -2274,22 +2307,31 @@ export function loadShields() { Color.shields.blue, Color.shields.white ); - shields["US:TX:Express:Toll"] = banneredShield(shields["US:TX:Toll"], [ - "EXPR", - ]); - shields["US:TX:Loop:Toll"] = banneredShield(shields["US:TX:Toll"], ["LOOP"]); - shields["US:TX:Loop:Express:Toll"] = banneredShield(shields["US:TX:Toll"], [ - "EXPR", - "LOOP", - ]); + shields["US:TX:Express:Toll"] = banneredShield( + shields["US:TX:Toll"], + ["EXPR"], + Color.shields.blue + ); + shields["US:TX:Loop:Toll"] = banneredShield( + shields["US:TX:Toll"], + ["LOOP"], + Color.shields.blue + ); + shields["US:TX:Loop:Express:Toll"] = banneredShield( + shields["US:TX:Toll"], + ["EXPR", "LOOP"], + Color.shields.blue + ); shields["US:TX:CTRMA"] = roundedRectShield( Color.shields.blue, Color.shields.yellow, Color.shields.white ); - shields["US:TX:CTRMA:Express"] = banneredShield(shields["US:TX:CTRMA"], [ - "EXPR", - ]); + shields["US:TX:CTRMA:Express"] = banneredShield( + shields["US:TX:CTRMA"], + ["EXPR"], + Color.shields.blue + ); shields["US:TX:Montgomery:MCTRA"] = homePlateDownShield( 5, Color.shields.blue, @@ -2374,11 +2416,13 @@ export function loadShields() { ); shields["US:TX:Jackson"] = banneredShield( roundedRectShield(Color.shields.blue, Color.shields.white), - ["CR"] + ["CR"], + Color.shields.blue ); shields["US:TX:Andrews:Andrews:Loop"] = banneredShield( roundedRectShield(Color.shields.white, Color.shields.blue), - ["LOOP"] + ["LOOP"], + Color.shields.blue ); // Utah @@ -2428,7 +2472,12 @@ export function loadShields() { bottom: 2, }, }; - shields["US:VT:Alternate"] = banneredShield(shields["US:VT"], ["ALT"]); + shields["US:VT:Alternate"] = banneredShield( + shields["US:VT"], + ["ALT"], + Color.shields.green + ); + // Vermont routes town maintained sections - black and white ovals shields["US:VT:Town"] = ovalShield(Color.shields.white, Color.shields.black); @@ -3501,7 +3550,8 @@ export function loadShields() { ); shields[`AU:${state_or_territory}:ALT`] = banneredShield( roundedRectShield(Color.shields.green, Color.shields.yellow), - ["ALT"] + ["ALT"], + Color.shields.green ); shields[`AU:${state_or_territory}:ALT_NR`] = banneredShield( homePlateDownShield(5, Color.shields.white, Color.shields.black), @@ -3509,7 +3559,8 @@ export function loadShields() { ); shields[`AU:${state_or_territory}:ALT_S`] = banneredShield( fishheadDownShield(Color.shields.blue, Color.shields.white), - ["ALT"] + ["ALT"], + Color.shields.blue ); } );