diff --git a/shieldlib/README.md b/shieldlib/README.md index c1f4e6379..1a85ff789 100644 --- a/shieldlib/README.md +++ b/shieldlib/README.md @@ -127,8 +127,8 @@ You should create one definition entry for each network. The entry key must matc "drawFunc": "pentagon", "params": { "pointUp": false, - "offset": 5, - "angle": 0, + "yOffset": 5, + "sideAngle": 0, "fillColor": "white", "strokeColor": "black", "radius1": 2, @@ -299,9 +299,9 @@ If `shapeBlank` is specified, the shield will be drawn as a shape. This needs to The following `params` options can be specified: -- `angle` - indicates angle (in degrees) at which side edges deviate from vertical. Applies to `trapezoid`, `pentagon`, `hexagonHorizontal`, `octagonVertical`. +- `sideAngle` - indicates angle (in degrees) at which side edges deviate from vertical. Applies to `trapezoid`, `pentagon`, `hexagonHorizontal`, `octagonVertical`. - `fill` - specifies the internal fill color. -- `offset` - indicates height (in pixels) at which the bottom and/or top edges deviate from horizontal. Applies to `escutcheon`, `pentagon`, `hexagonVertical`, `octagonVertical`. +- `yOffset` - indicates height (in pixels) at which the bottom and/or top edges deviate from horizontal. Applies to `escutcheon`, `pentagon`, `hexagonVertical`, `octagonVertical`. - `outline` - specifies the outline color. - `outlineWidth` - specifies the width of the outline. - `pointUp` - applies to several shape types and specifies whether the pointy side is up. diff --git a/shieldlib/src/screen_gfx.ts b/shieldlib/src/screen_gfx.ts index 225b43420..e3fe2b55c 100644 --- a/shieldlib/src/screen_gfx.ts +++ b/shieldlib/src/screen_gfx.ts @@ -3,7 +3,7 @@ import { StyleImage } from "maplibre-gl"; import rgba from "color-rgba"; const defaultFontFamily = '"sans-serif-condensed", "Arial Narrow", sans-serif'; -export const shieldFont = (size: string, fontFamily: string) => +export const shieldFont = (size: number, fontFamily: string) => `bold ${size}px ${fontFamily || defaultFontFamily}`; export const fontSizeThreshold = 12; diff --git a/shieldlib/src/shield_canvas_draw.ts b/shieldlib/src/shield_canvas_draw.ts index 11057815c..6cfa8154f 100644 --- a/shieldlib/src/shield_canvas_draw.ts +++ b/shieldlib/src/shield_canvas_draw.ts @@ -24,8 +24,8 @@ export function computeWidth( } let rectWidth = params.rectWidth == undefined ? null : params.rectWidth; - let angle = params.sideAngle == undefined ? 0 : params.sideAngle; - let tangent = Math.tan(angle); + let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; + let tangent = Math.tan(sideAngle); if (rectWidth == null) { let shieldWidth = @@ -36,8 +36,8 @@ export function computeWidth( //Shape-specific width adjustments switch (shape) { case "pentagon": - let offset = params.yOffset == undefined ? 0 : params.yOffset; - shieldWidth += ((r.shieldSize() - r.px(offset)) * tangent) / 2; + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; + shieldWidth += ((r.shieldSize() - r.px(yOffset)) * tangent) / 2; break; case "trapezoid": shieldWidth += (r.shieldSize() * tangent) / 2; @@ -164,7 +164,7 @@ function escutcheon( params: ShapeBlankParams, ref: string ) { - let offset = params.yOffset == undefined ? 0 : params.yOffset; + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -175,7 +175,7 @@ function escutcheon( let lineThick = r.px(outlineWidth); let lineWidth = lineThick / 2; let drawRadius = r.px(radius); - let drawOffset = r.px(offset); + let drawOffset = r.px(yOffset); let x0 = lineWidth; let x5 = width - lineWidth; @@ -346,16 +346,16 @@ function trapezoid( ) { let shortSideUp = params.shortSideUp == undefined ? false : params.shortSideUp; - let angle = params.sideAngle == undefined ? 0 : params.sideAngle; + 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 angleSign = shortSideUp ? -1 : 1; - let sine = Math.sin(angle); - let cosine = Math.cos(angle); - let tangent = Math.tan(angle); + let sine = Math.sin(sideAngle); + let cosine = Math.cos(sideAngle); + let tangent = Math.tan(sideAngle); let width = computeWidth(r, params, ref, "trapezoid"); @@ -476,8 +476,8 @@ function pentagon( ref: string ) { let pointUp = params.pointUp == undefined ? true : params.pointUp; - let offset = params.yOffset == undefined ? 0 : params.yOffset; - let angle = params.sideAngle == undefined ? 0 : params.sideAngle; + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; + 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 radius1 = params.radius1 == undefined ? 0 : params.radius1; @@ -485,9 +485,9 @@ function pentagon( let outlineWidth = params.outlineWidth == undefined ? 1 : params.outlineWidth; let angleSign = pointUp ? -1 : 1; - let sine = Math.sin(angle); - let cosine = Math.cos(angle); - let tangent = Math.tan(angle); + let sine = Math.sin(sideAngle); + let cosine = Math.cos(sideAngle); + let tangent = Math.tan(sideAngle); let width = computeWidth(r, params, ref, "pentagon"); @@ -495,7 +495,7 @@ function pentagon( let lineWidth = lineThick / 2; let drawRadius1 = r.px(radius1); let drawRadius2 = r.px(radius2); - let drawOffset = r.px(offset); + let drawOffset = r.px(yOffset); let x0 = lineWidth; let x8 = width - lineWidth; @@ -510,10 +510,10 @@ function pentagon( let offsetAngle = Math.atan(drawOffset / (x4 - x0)); - let halfComplementAngle1 = (Math.PI / 2 - offsetAngle + angle) / 2; + let halfComplementAngle1 = (Math.PI / 2 - offsetAngle + sideAngle) / 2; let halfComplementTangent1 = Math.tan(halfComplementAngle1); - let halfComplementAngle2 = (Math.PI / 2 - angle) / 2; + let halfComplementAngle2 = (Math.PI / 2 - sideAngle) / 2; let halfComplementTangent2 = Math.tan(halfComplementAngle2); let x1 = x0 + drawRadius1 * halfComplementTangent1 * sine; @@ -548,7 +548,7 @@ function hexagonVertical( params: ShapeBlankParams, ref: string ) { - let offset = params.yOffset == undefined ? 0 : params.yOffset; + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -559,7 +559,7 @@ function hexagonVertical( let lineThick = r.px(outlineWidth); let lineWidth = lineThick / 2; let drawRadius = r.px(radius); - let drawOffset = r.px(offset); + let drawOffset = r.px(yOffset); let x0 = lineWidth; let x2 = width - lineWidth; @@ -668,23 +668,23 @@ function octagonVertical( params: ShapeBlankParams, ref: string ) { - let offset = params.yOffset == undefined ? 0 : params.yOffset; - let angle = params.sideAngle == undefined ? 0 : params.sideAngle; + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; + 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 sine = Math.sin(sideAngle); + let cosine = Math.cos(sideAngle); + let tangent = Math.tan(sideAngle); let width = computeWidth(r, params, ref); let lineThick = r.px(outlineWidth); let lineWidth = lineThick / 2; let drawRadius = r.px(radius); - let drawOffset = r.px(offset); + let drawOffset = r.px(yOffset); let x0 = lineWidth; let x10 = width - lineWidth; @@ -707,13 +707,15 @@ function octagonVertical( let offsetSine = Math.sin(offsetAngle); let offsetCosine = Math.cos(offsetAngle); - let halfComplementAngle = (Math.PI / 2 - angle - offsetAngle) / 2; + let halfComplementAngle = (Math.PI / 2 - sideAngle - offsetAngle) / 2; let halfComplementCosine = Math.cos(halfComplementAngle); let dx = - (drawRadius * Math.cos(angle + halfComplementAngle)) / halfComplementCosine; + (drawRadius * Math.cos(sideAngle + halfComplementAngle)) / + halfComplementCosine; let dy = - (drawRadius * Math.sin(angle + halfComplementAngle)) / halfComplementCosine; + (drawRadius * Math.sin(sideAngle + halfComplementAngle)) / + halfComplementCosine; let x2 = x3 + dx - drawRadius * cosine; let x4 = x3 + dx - drawRadius * offsetSine; diff --git a/shieldlib/src/shield_helper.d.ts b/shieldlib/src/shield_helper.d.ts index 64bc9f72e..85c942bee 100644 --- a/shieldlib/src/shield_helper.d.ts +++ b/shieldlib/src/shield_helper.d.ts @@ -24,7 +24,7 @@ export declare function roundedRectShield( ): ShieldDefinition; export declare function escutcheonDownShield( - offset: number, + yOffset: number, fillColor: string, strokeColor: string, textColor: string, @@ -48,16 +48,16 @@ export declare function triangleDownShield( ): ShieldDefinition; export declare function trapezoidDownShield( - angle: number, + sideAngle: number, fillColor: string, strokeColor: string, textColor: string, radius: number, rectWidth: number -); +): ShieldDefinition; export declare function trapezoidUpShield( - angle: number, + sideAngle: number, fillColor: string, strokeColor: string, textColor: string, @@ -74,8 +74,8 @@ export declare function diamondShield( ): ShieldDefinition; export declare function pentagonUpShield( - offset: number, - angle: number, + yOffset: number, + sideAngle: number, fillColor: string, strokeColor: string, textColor: string, @@ -85,7 +85,7 @@ export declare function pentagonUpShield( ): ShieldDefinition; export declare function homePlateDownShield( - offset: number, + yOffset: number, fillColor: string, strokeColor: string, textColor: string, @@ -95,7 +95,7 @@ export declare function homePlateDownShield( ): ShieldDefinition; export declare function homePlateUpShield( - offset: number, + yOffset: number, fillColor: string, strokeColor: string, textColor: string, @@ -105,7 +105,7 @@ export declare function homePlateUpShield( ): ShieldDefinition; export declare function hexagonVerticalShield( - offset: number, + yOffset: number, fillColor: string, strokeColor: string, textColor: string, @@ -113,19 +113,8 @@ export declare function hexagonVerticalShield( rectWidth: number ): ShieldDefinition; -/** - * Draws a shield with a horizontally-aligned hexagon background - * - * @param {*} angle - Angle (in degrees) at which sides deviate from vertical - * @param {*} fillColor - Color of hexagon background fill - * @param {*} strokeColor - Color of hexagon outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of hexagon (defaults to 2) - * @param {*} rectWidth - Width of hexagon (defaults to variable-width) - * @returns a shield definition object - */ export declare function hexagonHorizontalShield( - angle: number, + sideAngle: number, fillColor: string, strokeColor: string, textColor: string, @@ -134,8 +123,8 @@ export declare function hexagonHorizontalShield( ): ShieldDefinition; export declare function octagonVerticalShield( - offset: number, - angle: number, + yOffset: number, + sideAngle: number, fillColor: string, strokeColor: string, textColor: string, diff --git a/shieldlib/src/shield_helper.ts b/shieldlib/src/shield_helper.ts index d8e6aebe0..8b34c9e9d 100644 --- a/shieldlib/src/shield_helper.ts +++ b/shieldlib/src/shield_helper.ts @@ -568,7 +568,7 @@ export function hexagonVerticalShield( /** * Draws a shield with a horizontally-aligned hexagon background * - * @param {*} angle - Angle (in degrees) at which sides deviate from vertical + * @param {*} sideAngle - Angle (in degrees) at which sides deviate from vertical * @param {*} fillColor - Color of hexagon background fill * @param {*} strokeColor - Color of hexagon outline stroke * @param {*} textColor - Color of text (defaults to strokeColor) @@ -577,14 +577,14 @@ export function hexagonVerticalShield( * @returns a shield definition object */ export function hexagonHorizontalShield( - angle: number, + sideAngle: number, fillColor: string, strokeColor: string, textColor: string, radius: number, rectWidth: number ): ShieldDefinition { - let angleInRadians = (angle * Math.PI) / 180; + let angleInRadians = (sideAngle * Math.PI) / 180; textColor = textColor ?? strokeColor; radius = radius ?? 2; return { diff --git a/shieldlib/src/shield_text.ts b/shieldlib/src/shield_text.ts new file mode 100644 index 000000000..e9dc755ef --- /dev/null +++ b/shieldlib/src/shield_text.ts @@ -0,0 +1,473 @@ +"use strict"; + +import * as Gfx from "./screen_gfx.js"; +import { ShieldRenderingContext } from "./shield_renderer.js"; +import { + BoxPadding, + ShieldDefinition, + TextLayout, + TextLayoutParameters, +} from "./types.js"; + +const VerticalAlignment = { + Middle: "middle", + Top: "top", + Bottom: "bottom", +} as const; + +type VerticalAlignmentType = + (typeof VerticalAlignment)[keyof typeof VerticalAlignment]; + +interface Dimension { + width: number; + height: number; +} + +type TextLayoutScaler = ( + availSize: Dimension, + textSize: Dimension, + options?: TextLayoutParameters +) => TextTransform; + +interface TextTransform { + scale: number; + valign: VerticalAlignmentType; +} + +interface TextPlacement { + xBaseline: number; + yBaseline: number; + fontPx: number; +} + +let noPadding: BoxPadding = { + top: 0, + bottom: 0, + left: 0, + 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; + 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: Dimension, + textBounds: Dimension +): TextTransform { + return { + scale: ellipseScale(spaceBounds, textBounds), + valign: VerticalAlignment.Middle, + }; +} + +function southHalfEllipseTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { + return { + scale: ellipseScale(spaceBounds, { + //Turn ellipse 90 degrees + height: textBounds.width / 2, + width: textBounds.height, + }), + valign: VerticalAlignment.Top, + }; +} + +function rectTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { + var scaleHeight = spaceBounds.height / textBounds.height; + var scaleWidth = spaceBounds.width / textBounds.width; + + return { + scale: Math.min(scaleWidth, scaleHeight), + valign: VerticalAlignment.Middle, + }; +} + +function roundedRectTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension, + options: TextLayoutParameters +): TextTransform { + //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: Dimension, + textBounds: Dimension +): TextTransform { + 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: Dimension, + textBounds: Dimension +): TextTransform { + 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: ShieldRenderingContext, + text: string, + padding: BoxPadding, + bounds: Dimension, + 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; + + 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; + + let yBaseline: number; + + 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, + yBaseline, + fontPx: fontSize, + }; +} + +const defaultDefForLayout: ShieldDefinition = { + padding: { + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + shapeBlank: { + drawFunc: "rectangle", + params: { + fillColor: "white", + strokeColor: "black", + }, + }, +}; + +/** + * 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: ShieldRenderingContext, + text: string, + def: ShieldDefinition, + bounds: Dimension +): TextPlacement { + //FIX + if (def == null) { + def = defaultDefForLayout; + } + + var padding = def.padding || noPadding; + + 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: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + textLayout: TextPlacement +): void { + //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: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + textLayout: TextPlacement +): void { + //Stroke color is set by strokeStyle + configureShieldText(r, ctx, textLayout); + + ctx.shadowColor = ctx.strokeStyle.toString(); + ctx.shadowBlur = 0; + ctx.lineWidth = r.px(2); + + ctx.strokeText(text, textLayout.xBaseline, textLayout.yBaseline); + ctx.shadowColor = null; + ctx.shadowBlur = null; +} + +function configureShieldText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + textLayout: TextPlacement +): void { + 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: 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, + fontSize: number +): number { + var ctx = r.emptySprite(); //dummy canvas + ctx.font = Gfx.shieldFont(fontSize, r.options.shieldFont); + return Math.ceil(ctx.measureText(text).width); +} + +//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: string, fxn: TextLayoutScaler): void { + 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/types.ts b/shieldlib/src/types.ts index 5912be5de..2c1038dbb 100644 --- a/shieldlib/src/types.ts +++ b/shieldlib/src/types.ts @@ -27,6 +27,8 @@ export interface ShieldDefinitionBase { banners?: string[]; /** If true, no next should be drawn on this shield */ notext?: boolean; + /** Maximum size of shield text */ + maxFontSize?: number; } /** @@ -98,12 +100,15 @@ export interface ShapeBlankParams { sideAngle?: number; } -/** Parameters for laying out text on a shield background */ +/** Definition for laying out text on a shield background */ export interface TextLayout { constraintFunc: string; - options?: { - radius: number; - }; + options?: TextLayoutParameters; +} + +/** Options for text layout on a shield */ +export interface TextLayoutParameters { + radius: number; } /**