Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Per-shield banner colors #941

Merged
merged 8 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions shieldlib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions shieldlib/src/custom_shields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function paBelt(
r: ShieldRenderingContext,
ctx: CanvasRenderingContext2D,
params: ShapeBlankParams
) {
): number {
ShieldDraw.roundedRectangle(r, ctx, {
fillColor: "white",
strokeColor: "black",
Expand Down Expand Up @@ -38,15 +38,15 @@ export function paBelt(

ctx.lineWidth = lineWidth;
ctx.stroke();
return ctx;
return 20;
}

// Special case for Branson color-coded routes
export function bransonRoute(
r: ShieldRenderingContext,
ctx: CanvasRenderingContext2D,
params: ShapeBlankParams
) {
): number {
ShieldDraw.roundedRectangle(r, ctx, {
fillColor: "#006747",
strokeColor: "white",
Expand All @@ -73,7 +73,7 @@ export function bransonRoute(

ctx.lineWidth = lineWidth;
ctx.stroke();
return ctx;
return 20;
}

export function loadCustomShields() {
Expand Down
38 changes: 9 additions & 29 deletions shieldlib/src/shield.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
179 changes: 179 additions & 0 deletions shieldlib/src/shield_banner.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading