UX: Lots of wizard canvas rendering improvements

* Make sure bold body font loads before rendering otherwise
  the 1/20 post count on the timeline will use wrong font
* Fix pill button rendering on homepage preview, now all
  the widths resize based on the font size/width and all
  buttons depend on the X position of the button beforehand
* Fix the table header labels overlapping by giving them more
  room based on col position
* Moving hardcoded label text into I18n
This commit is contained in:
Martin Brennan 2024-12-06 16:58:54 +10:00
parent 9c6e5440d8
commit 7c92402e15
No known key found for this signature in database
GPG Key ID: BD981EFEEC8F5675
9 changed files with 135 additions and 71 deletions

View File

@ -83,7 +83,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
] ]
); );
ctx.font = `Bold ${bodyFontSize * 1.3}em '${font}'`; ctx.font = `700 ${bodyFontSize * 1.3}em '${font}'`;
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.fillText(category.name, boxStartX + boxWidth / 2, boxStartY + 25); ctx.fillText(category.name, boxStartX + boxWidth / 2, boxStartY + 25);
@ -166,7 +166,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
// Categories // Categories
this.categories().forEach((category, idx) => { this.categories().forEach((category, idx) => {
const textPos = y + categoryHeight * 0.35; const textPos = y + categoryHeight * 0.35;
ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`; ctx.font = `700 ${bodyFontSize * 1.1}em '${font}'`;
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.fillText(category.name, cols[0], textPos); ctx.fillText(category.name, cols[0], textPos);
@ -262,7 +262,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
// Categories // Categories
this.categories().forEach((category, idx) => { this.categories().forEach((category, idx) => {
const textPos = y + categoryHeight * 0.35; const textPos = y + categoryHeight * 0.35;
ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`; ctx.font = `700 ${bodyFontSize * 1.1}em '${font}'`;
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.fillText(category.name, cols[0], textPos); ctx.fillText(category.name, cols[0], textPos);
@ -310,7 +310,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
); );
ctx.fillText(title, cols[3], textPos); ctx.fillText(title, cols[3], textPos);
ctx.font = `Bold ${bodyFontSize}em '${font}'`; ctx.font = `700 ${bodyFontSize}em '${font}'`;
ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[4], textPos); ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[4], textPos);
ctx.font = `${bodyFontSize}em '${font}'`; ctx.font = `${bodyFontSize}em '${font}'`;
ctx.fillText(`1h`, cols[4], textPos + topicHeight * 0.4); ctx.fillText(`1h`, cols[4], textPos + topicHeight * 0.4);
@ -318,7 +318,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = category.color; ctx.fillStyle = category.color;
const badgeSize = topicHeight * 0.1; const badgeSize = topicHeight * 0.1;
ctx.font = `Bold ${bodyFontSize * 0.5}em '${font}'`; ctx.font = `700 ${bodyFontSize * 0.5}em '${font}'`;
ctx.rect( ctx.rect(
cols[3] + margin * 0.25, cols[3] + margin * 0.25,
y + topicHeight * 0.65, y + topicHeight * 0.65,
@ -383,17 +383,33 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.stroke(); ctx.stroke();
}; };
const cols = [0.02, 0.66, 0.8, 0.87, 0.93].map((c) => c * width); const cols = [0.02, 0.66, 0.75, 0.83, 0.9].map((c) => c * width);
// Headings // Headings
const headingY = height * 0.33; const headingY = height * 0.33;
ctx.fillStyle = textColor; ctx.fillStyle = textColor;
ctx.font = `${bodyFontSize * 0.9}em '${font}'`; ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
ctx.fillText("Topic", cols[0], headingY); ctx.fillText(
ctx.fillText("Replies", cols[2], headingY); i18n("wizard.homepage_preview.table_headers.topic"),
ctx.fillText("Views", cols[3], headingY); cols[0],
ctx.fillText("Activity", cols[4], headingY); headingY
);
ctx.fillText(
i18n("wizard.homepage_preview.table_headers.replies"),
cols[2],
headingY
);
ctx.fillText(
i18n("wizard.homepage_preview.table_headers.views"),
cols[3],
headingY
);
ctx.fillText(
i18n("wizard.homepage_preview.table_headers.activity"),
cols[4],
headingY
);
// Topics // Topics
let y = headingY + rowHeight / 2.6; let y = headingY + rowHeight / 2.6;
@ -412,7 +428,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = category.color; ctx.fillStyle = category.color;
const badgeSize = rowHeight * 0.15; const badgeSize = rowHeight * 0.15;
ctx.font = `Bold ${bodyFontSize * 0.75}em '${font}'`; ctx.font = `700 ${bodyFontSize * 0.75}em '${font}'`;
ctx.rect(cols[0] + 4, y + rowHeight * 0.6, badgeSize, badgeSize); ctx.rect(cols[0] + 4, y + rowHeight * 0.6, badgeSize, badgeSize);
ctx.fill(); ctx.fill();

View File

@ -116,7 +116,7 @@ export default class PreviewBase extends Component {
}); });
}); });
Promise.all( return Promise.all(
fontFaces.map((fontFace) => fontFaces.map((fontFace) =>
fontFace.load().then((loadedFont) => { fontFace.load().then((loadedFont) => {
document.fonts.add(loadedFont); document.fonts.add(loadedFont);
@ -135,7 +135,7 @@ export default class PreviewBase extends Component {
this.loadingFontVariants = false; this.loadingFontVariants = false;
}); });
} else if (this.loadedFonts.has(font.id)) { } else if (this.loadedFonts.has(font.id)) {
this.triggerRepaint(); return Promise.resolve(this.triggerRepaint());
} }
} }
@ -162,8 +162,14 @@ export default class PreviewBase extends Component {
reload() { reload() {
Promise.all([this.loadFonts(), this.loadImages()]).then(() => { Promise.all([this.loadFonts(), this.loadImages()]).then(() => {
this.loaded = true; // NOTE: This must be done otherwise the "bold" variant of the body font
this.triggerRepaint(); // will not be loaded for some reason before rendering the canvas.
//
// The header font does not suffer from this issue.
this.loadFontVariants(this.wizard.font).then(() => {
this.loaded = true;
this.triggerRepaint();
});
}); });
} }
@ -283,17 +289,16 @@ export default class PreviewBase extends Component {
avatarSize, avatarSize,
avatarSize avatarSize
); );
// accounts for hard-set color variables in solarized themes // accounts for hard-set color variables in solarized themes
ctx.fillStyle = ctx.fillStyle =
colors.primary_low_mid || colors.primary_low_mid ||
darkLightDiff(colors.primary, colors.secondary, 45, 55); darkLightDiff(colors.primary, colors.secondary, 45, 55);
const pathScale = this.headerHeight / 1200; const pathScale = this.headerHeight / 1200;
// search icon SVG path
const searchIcon = new Path2D( const searchIcon = new Path2D(
"M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z" "M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
); );
// hamburger icon
const hamburgerIcon = new Path2D( const hamburgerIcon = new Path2D(
"M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z" "M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
); );
@ -321,100 +326,100 @@ export default class PreviewBase extends Component {
const { ctx } = this; const { ctx } = this;
const categoriesSize = headerHeight * 2; const badgeHeight = headerHeight * 2 * 0.25;
const badgeHeight = categoriesSize * 0.25;
const headerMargin = headerHeight * 0.2; const headerMargin = headerHeight * 0.2;
const fontSize = Math.round(badgeHeight * 0.5);
ctx.font = `${fontSize}px '${font}'`;
const allCategoriesText = i18n(
"wizard.homepage_preview.nav_buttons.all_categories"
);
const categoriesWidth = ctx.measureText(allCategoriesText).width;
const categoriesBoxWidth = categoriesWidth + headerMargin * 2;
// Box around "all categories >"
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = colors.primary; ctx.strokeStyle = colors.primary;
ctx.lineWidth = 0.5; ctx.lineWidth = 0.5;
ctx.rect( ctx.rect(
headerMargin, headerMargin,
headerHeight + headerMargin, headerHeight + headerMargin,
categoriesSize, categoriesBoxWidth,
badgeHeight badgeHeight
); );
ctx.stroke(); ctx.stroke();
const fontSize = Math.round(badgeHeight * 0.5);
ctx.font = `${fontSize}px '${font}'`;
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.fillText( ctx.fillText(
"all categories", allCategoriesText,
headerMargin * 1.5, headerMargin * 1.5,
headerHeight + headerMargin * 1.4 + fontSize headerHeight + headerMargin * 1.4 + fontSize
); );
// Caret (>) at the end of "all categories" box
const pathScale = badgeHeight / 1000; const pathScale = badgeHeight / 1000;
// caret icon
const caretIcon = new Path2D( const caretIcon = new Path2D(
"M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z" "M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
); );
ctx.save(); ctx.save();
ctx.translate( ctx.translate(
categoriesSize - headerMargin / 4, categoriesBoxWidth,
headerHeight + headerMargin + badgeHeight / 4 headerHeight + headerMargin + badgeHeight / 4
); );
ctx.scale(pathScale, pathScale); ctx.scale(pathScale, pathScale);
ctx.fill(caretIcon); ctx.fill(caretIcon);
ctx.restore(); ctx.restore();
const text = opts.categories ? "Categories" : "Latest"; // First top menu item
const firstTopMenuItemText = opts.categories
? i18n("wizard.homepage_preview.nav_buttons.categories")
: i18n("wizard.homepage_preview.nav_buttons.latest");
const newText = i18n("wizard.homepage_preview.nav_buttons.new");
const unreadText = i18n("wizard.homepage_preview.nav_buttons.unread");
const topText = i18n("wizard.homepage_preview.nav_buttons.top");
const hotText = i18n("wizard.homepage_preview.nav_buttons.hot");
const activeWidth = categoriesSize * (opts.categories ? 0.8 : 0.55);
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = colors.tertiary; ctx.fillStyle = colors.tertiary;
ctx.rect( ctx.rect(
headerMargin * 2 + categoriesSize, categoriesBoxWidth + headerMargin * 2,
headerHeight + headerMargin, headerHeight + headerMargin,
activeWidth, ctx.measureText(firstTopMenuItemText).width + headerMargin * 2,
badgeHeight badgeHeight
); );
ctx.fill(); ctx.fill();
ctx.font = `${fontSize}px '${font}'`; ctx.font = `${fontSize}px '${font}'`;
ctx.fillStyle = colors.secondary; ctx.fillStyle = colors.secondary;
let x = headerMargin * 3.0 + categoriesSize; const pillButtonTextY = headerHeight + headerMargin * 1.4 + fontSize;
const firstTopMenuItemX = headerMargin * 3.0 + categoriesBoxWidth;
ctx.fillText( ctx.fillText(
text, firstTopMenuItemText,
x - headerMargin * 0.1, firstTopMenuItemX,
headerHeight + headerMargin * 1.5 + fontSize pillButtonTextY,
ctx.measureText(firstTopMenuItemText).width
); );
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
x += categoriesSize * (opts.categories ? 0.8 : 0.6);
ctx.fillText("New", x, headerHeight + headerMargin * 1.5 + fontSize);
x += categoriesSize * 0.4; const newTextX =
ctx.fillText("Unread", x, headerHeight + headerMargin * 1.5 + fontSize); firstTopMenuItemX +
ctx.measureText(firstTopMenuItemText).width +
headerMargin * 2.0;
ctx.fillText(newText, newTextX, pillButtonTextY);
x += categoriesSize * 0.6; const unreadTextX =
ctx.fillText("Top", x, headerHeight + headerMargin * 1.5 + fontSize); newTextX + ctx.measureText(newText).width + headerMargin * 2.0;
} ctx.fillText(unreadText, unreadTextX, pillButtonTextY);
resizeTextLinesToFitRect( const topTextX =
textLines, unreadTextX + ctx.measureText(unreadText).width + headerMargin * 2.0;
rectWidth, ctx.fillText(topText, topTextX, pillButtonTextY);
ctx,
fontSize,
font,
renderCallback
) {
const maxLengthLine = textLines.reduce((a, b) =>
a.length > b.length ? a : b
);
let fontSizeDecreaseMultiplier = 1; const hotTextX =
while (ctx.measureText(maxLengthLine).width > rectWidth) { topTextX + ctx.measureText(topText).width + headerMargin * 2.0;
fontSizeDecreaseMultiplier -= 0.1; ctx.fillText(hotText, hotTextX, pillButtonTextY);
ctx.font = `${fontSize * fontSizeDecreaseMultiplier}em '${font}'`;
}
for (let i = 0; i < textLines.length; i++) {
renderCallback(textLines[i], i);
}
} }
} }

View File

@ -2,7 +2,11 @@ import { action } from "@ember/object";
import { observes } from "@ember-decorators/object"; import { observes } from "@ember-decorators/object";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import { chooseDarker, darkLightDiff } from "../../../lib/preview"; import {
chooseDarker,
darkLightDiff,
resizeTextLinesToFitRect,
} from "../../../lib/preview";
import HomepagePreview from "./-homepage-preview"; import HomepagePreview from "./-homepage-preview";
import PreviewBaseComponent from "./-preview-base"; import PreviewBaseComponent from "./-preview-base";
@ -133,7 +137,7 @@ export default class Index extends PreviewBaseComponent {
// Topic title // Topic title
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.font = `bold ${titleFontSize}em '${headingFont}'`; ctx.font = `700 ${titleFontSize}em '${headingFont}'`;
ctx.fillText(i18n("wizard.previews.topic_title"), margin, height * 0.3); ctx.fillText(i18n("wizard.previews.topic_title"), margin, height * 0.3);
// Topic OP text // Topic OP text
@ -144,7 +148,7 @@ export default class Index extends PreviewBaseComponent {
const topicOp = i18n("wizard.homepage_preview.topic_ops.what_books"); const topicOp = i18n("wizard.homepage_preview.topic_ops.what_books");
const topicOpLines = topicOp.split("\n"); const topicOpLines = topicOp.split("\n");
this.resizeTextLinesToFitRect( resizeTextLinesToFitRect(
topicOpLines, topicOpLines,
timelineX - leftHandTextGutter, timelineX - leftHandTextGutter,
ctx, ctx,
@ -216,13 +220,14 @@ export default class Index extends PreviewBaseComponent {
// Timeline post count // Timeline post count
const postCountY = height * 0.3 + margin + 10; const postCountY = height * 0.3 + margin + 10;
ctx.font = `Bold ${bodyFontSize}em ${font}`; ctx.beginPath();
ctx.font = `700 ${bodyFontSize}em '${font}'`;
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.fillText("1 / 20", timelineX + margin / 2, postCountY); ctx.fillText("1 / 20", timelineX + margin / 2, postCountY);
// Timeline post date // Timeline post date
ctx.beginPath(); ctx.beginPath();
ctx.font = `${bodyFontSize * 0.9}em ${font}`; ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 70, 65); ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 70, 65);
ctx.fillText( ctx.fillText(
"Nov 22", "Nov 22",

View File

@ -138,3 +138,26 @@ export function drawHeader(ctx, colors, width, headerHeight) {
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();
} }
export function resizeTextLinesToFitRect(
textLines,
rectWidth,
ctx,
fontSize,
font,
renderCallback
) {
const maxLengthLine = textLines.reduce((a, b) =>
a.length > b.length ? a : b
);
let fontSizeDecreaseMultiplier = 1;
while (ctx.measureText(maxLengthLine).width > rectWidth) {
fontSizeDecreaseMultiplier -= 0.1;
ctx.font = `${fontSize * fontSizeDecreaseMultiplier}em '${font}'`;
}
for (let i = 0; i < textLines.length; i++) {
renderCallback(textLines[i], i);
}
}

View File

@ -72,7 +72,7 @@ class SiteSetting < ActiveRecord::Base
end end
def self.top_menu_items def self.top_menu_items
top_menu.split("|").map { |menu_item| TopMenuItem.new(menu_item) } top_menu_map.map { |menu_item| TopMenuItem.new(menu_item) }
end end
def self.homepage def self.homepage

View File

@ -7390,6 +7390,14 @@ en:
homepage_preview: "Homepage preview" homepage_preview: "Homepage preview"
homepage_preview: homepage_preview:
nav_buttons:
all_categories: "all categories"
new: "New"
unread: "Unread"
top: "Top"
latest: "Latest"
hot: "Hot"
categories: "Categories"
topic_titles: topic_titles:
what_books: "What books are you reading?" what_books: "What books are you reading?"
what_movies: "What movies have you seen recently?" what_movies: "What movies have you seen recently?"
@ -7409,3 +7417,8 @@ en:
icebreakers: "Icebreakers" icebreakers: "Icebreakers"
news: "News" news: "News"
site_feedback: "Site Feedback" site_feedback: "Site Feedback"
table_headers:
topic: "Topic"
replies: "Replies"
views: "Views"
activity: "Activity"

View File

@ -5423,6 +5423,8 @@ en:
choices: choices:
latest: latest:
label: "Latest Topics" label: "Latest Topics"
hot:
label: "Hot Topics"
categories_only: categories_only:
label: "Categories Only" label: "Categories Only"
categories_with_featured_topics: categories_with_featured_topics:

View File

@ -222,7 +222,7 @@ module Stylesheet
) )
contents << <<~CSS contents << <<~CSS
@font-face { @font-face {
font-family: #{font[:name]}; font-family: '#{font[:name]}';
src: #{src}; src: #{src};
font-weight: #{variant[:weight]}; font-weight: #{variant[:weight]};
} }

View File

@ -191,7 +191,7 @@ class Wizard
current = current =
( (
if SiteSetting.top_menu.starts_with?("categories") if SiteSetting.top_menu_map.first == "categories"
SiteSetting.desktop_category_page_style SiteSetting.desktop_category_page_style
else else
"latest" "latest"
@ -214,8 +214,8 @@ class Wizard
updater.update_setting(:base_font, updater.fields[:body_font]) updater.update_setting(:base_font, updater.fields[:body_font])
updater.update_setting(:heading_font, updater.fields[:heading_font]) updater.update_setting(:heading_font, updater.fields[:heading_font])
top_menu = SiteSetting.top_menu.split("|") top_menu = SiteSetting.top_menu_map
if updater.fields[:homepage_style] == "latest" && top_menu[0] != "latest" if updater.fields[:homepage_style] == "latest" && top_menu.first != "latest"
top_menu.delete("latest") top_menu.delete("latest")
top_menu.insert(0, "latest") top_menu.insert(0, "latest")
elsif updater.fields[:homepage_style] != "latest" elsif updater.fields[:homepage_style] != "latest"