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

Fonts loaded by loadFont() cannot be used in CSS styles #2750

Open
nickmcintyre opened this issue Dec 21, 2023 · 4 comments
Open

Fonts loaded by loadFont() cannot be used in CSS styles #2750

nickmcintyre opened this issue Dec 21, 2023 · 4 comments
Labels
Area:Preview For features and bugs relating to the embedded preview sketch Bug

Comments

@nickmcintyre
Copy link
Member

p5.js version

1.9.0

What is your operating system?

Mac OS

Web browser and version

Chrome 120, Firefox 120, Safari 17.2

Actual Behavior

When I load a font with loadFont(), I can't use it to style HTML elements.

Expected Behavior

I should be able to set the font-family for HTML elements as shown in the last example.

Steps to reproduce

Here's a sketch that demonstrates the bug.

@raclim @lindapaiste @dhowe @paulaxisabel @SarveshLimaye @SkylerW99 @BamaCharanChhandogi @Obi-Engine10 @hannahvy @singshris @hiddenenigma I'm happy to help with this where I can!

let font;

// Load the font.
function preload() {
  font = loadFont("assets/PressStart2P-Regular.ttf");
}

function setup() {
  createCanvas(400, 400);

  // Paint the background.
  background(0);

  // Use the font in the canvas.
  fill(255);
  textAlign(CENTER);
  textFont(font, 22);
  text("Are you in?", width / 2, height / 2);

  // Create a <button> element.
  let button = createButton("Yes");
  button.size(100);
  button.position(width / 2 - 50, height / 2 + 30);
  
  // Style the button.
  button.style("background-color", "black");
  button.style("color", "white");
  button.style("border-radius", "10px");
  button.style("padding", "5px");

  // Try to set the button's font-family.

  // This works locally.
  button.style("font-family", "PressStart2P-Regular");

  // The code below makes the font-family work
  // in the p5.js Web Editor.
  
  // let f = new FontFace(
  //   "PressStart2P-Regular",
  //   "url(assets/PressStart2P-Regular.ttf)"
  // );
  // document.fonts.add(f);
}
@lindapaiste
Copy link
Collaborator

lindapaiste commented Dec 27, 2023

I would guess that the problem is in the part of the code where we map relative url paths like "assets/PressStart2P-Regular.ttf" to the absolute URL of the file on our AWS server. That all happens in the EmbedFrame.jsx file.

I was hoping this was a trivial fix where all we need to do is add .ttf to the extensions array. It's not that, as we do have .ttf in the MEDIA_FILE_REGEX.

I'm looking through the EmbedFrame.jsx file but there's a lot going on there. We need to dig through this file and make sure that strings like loadFont("assets/PressStart2P-Regular.ttf") get replaced.

function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
const elements = sketchDoc.querySelectorAll(`[${attr}]`);
const elementsArray = Array.prototype.slice.call(elements);
elementsArray.forEach((element) => {
if (element.getAttribute(attr).match(MEDIA_FILE_REGEX)) {
const resolvedFile = resolvePathToFile(element.getAttribute(attr), files);
if (resolvedFile && resolvedFile.url) {
element.setAttribute(attr, resolvedFile.url);
}
}
});
}
function resolveCSSLinksInString(content, files) {
let newContent = content;
let cssFileStrings = content.match(STRING_REGEX);
cssFileStrings = cssFileStrings || [];
cssFileStrings.forEach((cssFileString) => {
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = cssFileString.substr(1, cssFileString.length - 2);
const quoteCharacter = cssFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files);
if (resolvedFile) {
if (resolvedFile.url) {
newContent = newContent.replace(
cssFileString,
quoteCharacter + resolvedFile.url + quoteCharacter
);
}
}
}
});
return newContent;
}
function jsPreprocess(jsText) {
let newContent = jsText;
// check the code for js errors before sending it to strip comments
// or loops.
JSHINT(newContent);
if (JSHINT.errors.length === 0) {
newContent = decomment(newContent, {
ignore: /\/\/\s*noprotect/g,
space: true
});
newContent = loopProtect(newContent);
}
return newContent;
}
function resolveJSLinksInString(content, files) {
let newContent = content;
let jsFileStrings = content.match(STRING_REGEX);
jsFileStrings = jsFileStrings || [];
jsFileStrings.forEach((jsFileString) => {
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = jsFileString.substr(1, jsFileString.length - 2);
const quoteCharacter = jsFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files);
if (resolvedFile) {
if (resolvedFile.url) {
newContent = newContent.replace(
jsFileString,
quoteCharacter + resolvedFile.url + quoteCharacter
);
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
newContent = newContent.replace(
jsFileString,
quoteCharacter + resolvedFile.blobUrl + quoteCharacter
);
}
}
}
});
return jsPreprocess(newContent);
}
function resolveScripts(sketchDoc, files) {
const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach((script) => {
if (
script.getAttribute('src') &&
script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null
) {
const resolvedFile = resolvePathToFile(script.getAttribute('src'), files);
if (resolvedFile) {
if (resolvedFile.url) {
script.setAttribute('src', resolvedFile.url);
} else {
// in the future, when using y.js, could remake the blob for only the file(s)
// that changed
const blobUrl = createBlobUrl(resolvedFile);
script.setAttribute('src', blobUrl);
const blobPath = blobUrl.split('/').pop();
// objectUrls[blobUrl] = `${resolvedFile.filePath}${
// resolvedFile.filePath.length > 0 ? '/' : ''
// }${resolvedFile.name}`;
objectUrls[blobUrl] = `${resolvedFile.filePath}/${resolvedFile.name}`;
objectPaths[blobPath] = resolvedFile.name;
// script.setAttribute('data-tag', `${startTag}${resolvedFile.name}`);
// script.removeAttribute('src');
// script.innerHTML = resolvedFile.content; // eslint-disable-line
}
}
} else if (
!(
script.getAttribute('src') &&
script.getAttribute('src').match(EXTERNAL_LINK_REGEX)
) !== null
) {
script.setAttribute('crossorigin', '');
script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line
}
});
}
function resolveStyles(sketchDoc, files) {
const inlineCSSInHTML = sketchDoc.getElementsByTagName('style');
const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML);
inlineCSSInHTMLArray.forEach((style) => {
style.innerHTML = resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line
});
const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]');
const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML);
cssLinksInHTMLArray.forEach((css) => {
if (
css.getAttribute('href') &&
css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null
) {
const resolvedFile = resolvePathToFile(css.getAttribute('href'), files);
if (resolvedFile) {
if (resolvedFile.url) {
css.href = resolvedFile.url; // eslint-disable-line
} else {
const style = sketchDoc.createElement('style');
style.innerHTML = `\n${resolvedFile.content}`;
sketchDoc.head.appendChild(style);
css.parentElement.removeChild(css);
}
}
}
});
}
function resolveJSAndCSSLinks(files) {
const newFiles = [];
files.forEach((file) => {
const newFile = { ...file };
if (file.name.match(/.*\.js$/i)) {
newFile.content = resolveJSLinksInString(newFile.content, files);
} else if (file.name.match(/.*\.css$/i)) {
newFile.content = resolveCSSLinksInString(newFile.content, files);
}
newFiles.push(newFile);
});
return newFiles;
}
function addLoopProtect(sketchDoc) {
const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach((script) => {
script.innerHTML = jsPreprocess(script.innerHTML); // eslint-disable-line
});
}
function injectLocalFiles(files, htmlFile, options) {
const { basePath, gridOutput, textOutput } = options;
let scriptOffs = [];
objectUrls = {};
objectPaths = {};
const resolvedFiles = resolveJSAndCSSLinks(files);
const parser = new DOMParser();
const sketchDoc = parser.parseFromString(htmlFile.content, 'text/html');
const base = sketchDoc.createElement('base');
base.href = `${window.origin}${basePath}${basePath.length > 1 && '/'}`;
sketchDoc.head.appendChild(base);
resolvePathsForElementsWithAttribute('src', sketchDoc, resolvedFiles);
resolvePathsForElementsWithAttribute('href', sketchDoc, resolvedFiles);
// should also include background, data, poster, but these are used way less often
resolveScripts(sketchDoc, resolvedFiles);
resolveStyles(sketchDoc, resolvedFiles);
const accessiblelib = sketchDoc.createElement('script');
accessiblelib.setAttribute(
'src',
'https://cdn.jsdelivr.net/gh/processing/p5.accessibility@0.1.1/dist/p5-accessibility.js'
);
const accessibleOutputs = sketchDoc.createElement('section');
accessibleOutputs.setAttribute('id', 'accessible-outputs');
accessibleOutputs.setAttribute('aria-label', 'accessible-output');
if (textOutput || gridOutput) {
sketchDoc.body.appendChild(accessibleOutputs);
sketchDoc.body.appendChild(accessiblelib);
if (textOutput) {
const textSection = sketchDoc.createElement('section');
textSection.setAttribute('id', 'textOutput-content');
sketchDoc.getElementById('accessible-outputs').appendChild(textSection);
}
if (gridOutput) {
const gridSection = sketchDoc.createElement('section');
gridSection.setAttribute('id', 'tableOutput-content');
sketchDoc.getElementById('accessible-outputs').appendChild(gridSection);
}
}
const previewScripts = sketchDoc.createElement('script');
previewScripts.src = `${window.location.origin}${getConfig(
'PREVIEW_SCRIPTS_URL'
)}`;
previewScripts.setAttribute('crossorigin', '');
sketchDoc.head.appendChild(previewScripts);
const sketchDocString = `<!DOCTYPE HTML>\n${sketchDoc.documentElement.outerHTML}`;
scriptOffs = getAllScriptOffsets(sketchDocString);
const consoleErrorsScript = sketchDoc.createElement('script');
consoleErrorsScript.innerHTML = `
window.offs = ${JSON.stringify(scriptOffs)};
window.objectUrls = ${JSON.stringify(objectUrls)};
window.objectPaths = ${JSON.stringify(objectPaths)};
window.editorOrigin = '${getConfig('EDITOR_URL')}';
`;
addLoopProtect(sketchDoc);
sketchDoc.head.prepend(consoleErrorsScript);
return `<!DOCTYPE HTML>\n${sketchDoc.documentElement.outerHTML}`;
}

@PradeepJ4u
Copy link

Hi @nickmcintyre , I tried to replicate this issue and I was ablle to load the fonts mentioned. Attaching a video for the same. Attaching the font file and the code I used.

Please let me know if I am missing anything.
Press_Start_2P.zip
let font;

// Load the font.
function preload() {
font = loadFont("assets/PressStart2P-Regular.ttf");
}

function setup() {
createCanvas(400, 400);

// Paint the background.
background(0);

// Use the font in the canvas.
fill(255);
textAlign(CENTER);
textFont(font, 22);
text("Are you in?", width / 2, height / 2);

// Create a element.
let button = createButton("Yes");
button.size(100);
button.position(width / 2 - 50, height / 2 + 30);

// Style the button.
button.style("background-color", "black");
button.style("color", "white");
button.style("border-radius", "10px");
button.style("padding", "5px");

// Try to set the button's font-family.

// This works locally.
button.style("font-family", "PressStart2P-Regular");

// The code below makes the font-family work
// in the p5.js Web Editor.

// let f = new FontFace(
// "PressStart2P-Regular",
// "url(assets/PressStart2P-Regular.ttf)"
// );
// document.fonts.add(f);
}

p5-2750.mp4

@lindapaiste lindapaiste added the Area:Preview For features and bugs relating to the embedded preview sketch label Jan 9, 2024
@nickmcintyre
Copy link
Member Author

@PradeepJ4u thanks for looking into this. The issue is that the button's font-family isn't set.

@lindapaiste
Copy link
Collaborator

I looked into this more and I can see the problem!

image

The core p5.js loadFont function should be adding the font to the HTML document, as seen in the code here:
https://github.com/processing/p5.js/blob/37d3324b457b177319ed65468201f2806a66eff5/src/typography/loading_displaying.js#L166-L177

But in the editor the <style> that it adds is this:

<style>
@font-face {
font-family: 6b2bb864-90a5-4af7-be93-e0e4b3578674;
src: url(https://assets.editor.p5js.org/5b50ea9119d9bab3715ea4ce/6b2bb864-90a5-4af7-be93-e0e4b3578674.ttf);
}
</style>

Where the src part is fine, but the font-family is a problem. It should be the name of the font, but instead it's the name of the file on the S3 bucket. The loadFont function assumes that the name of the font matches the name of the file. But the actual file that we are loading is https://assets.editor.p5js.org/5b50ea9119d9bab3715ea4ce/6b2bb864-90a5-4af7-be93-e0e4b3578674.ttf.

I'm not sure if we would consider making any changes to the core p5.js code to support this use case.

I think that we probably need to do something about it on our end, or with some sort of plugin around the p5 code. It cannot be handled with a simple find and replace because that <style> tag does not exist in the document until the JavaScript code is executed. It's a tricky one.

@lindapaiste lindapaiste changed the title Fonts not loading Fonts loaded by loadFont() cannot be used in CSS styles Jan 14, 2024
Harshit-7373 added a commit to Harshit-7373/p5.js-web-editor that referenced this issue Mar 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area:Preview For features and bugs relating to the embedded preview sketch Bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants