diff --git a/packages/examples/src/examples/ui/ExampleUI.tsx b/packages/examples/src/examples/ui/ExampleUI.tsx index d6c8c132b..480263259 100644 --- a/packages/examples/src/examples/ui/ExampleUI.tsx +++ b/packages/examples/src/examples/ui/ExampleUI.tsx @@ -269,7 +269,7 @@ const createGame = () => { { name: "kenpixel", type: "fontface", - src: `url(${base}font/kenvector_future.woff2)`, + src: `${base}font/kenvector_future.woff2`, }, ]; diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 93a0e9042..a2fa336f3 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -3,6 +3,7 @@ ## [18.1.0] (melonJS 2) ### Added +- Loader: fontface assets now support `baseURL` — font paths are resolved consistently with other asset types - TextureAtlas: new `getAnimationSettings()` method for extending Sprite with texture atlas animations - TMX: automatically decompose concave collision polygons into convex triangles using earcut triangulation, instead of throwing an error - Ellipse: add rotation and matrix transform support (#583, #771) diff --git a/packages/melonjs/src/loader/loader.js b/packages/melonjs/src/loader/loader.js index 1b9c89974..e7ec7973a 100644 --- a/packages/melonjs/src/loader/loader.js +++ b/packages/melonjs/src/loader/loader.js @@ -119,7 +119,7 @@ export function setOptions(options) { * @name setBaseURL * @memberof loader * @public - * @param {string} type - "*", "audio", "video", "binary", "image", "json", "js", "tmx", "tsx" + * @param {string} type - "*", "audio", "video", "binary", "image", "json", "js", "tmx", "tsx", "fontface" * @param {string} [url="./"] - default base URL * @example * // change the base URL relative address for audio assets @@ -140,8 +140,7 @@ export function setBaseURL(type, url = "./") { baseURL["js"] = url; baseURL["tmx"] = url; baseURL["tsx"] = url; - // XXX ? - //baseURL["fontface"] = url; + baseURL["fontface"] = url; } } @@ -312,7 +311,7 @@ function onLoadingError(res) { * // JavaScript file * {name: "plugin", type: "js", src: "data/js/plugin.js"} * // Font Face - * { name: "'kenpixel'", type: "fontface", src: "url('data/font/kenvector_future.woff2')" } + * { name: "'kenpixel'", type: "fontface", src: "data/font/kenvector_future.woff2" } * // video resources * {name: "intro", type: "video", src: "data/video/"} */ @@ -385,7 +384,7 @@ export function setParser(type, parserFn) { * // JavaScript file * {name: "plugin", type: "js", src: "data/js/plugin.js"}, * // Font Face - * {name: "'kenpixel'", type: "fontface", src: "url('data/font/kenvector_future.woff2')"}, + * {name: "'kenpixel'", type: "fontface", src: "data/font/kenvector_future.woff2"}, * // video resources * {name: "intro", type: "video", src: "data/video/"}, * // base64 encoded video asset @@ -514,8 +513,21 @@ export function load(asset, onload, onerror) { initParsers(); } - // transform the url if necessary - if (typeof baseURL[asset.type] !== "undefined") { + // strip url() wrapper for fontface assets so baseURL can be prepended to the raw path + if (asset.type === "fontface" && typeof asset.src === "string") { + const urlMatch = asset.src.match(/^url\(\s*['"]?(.*?)['"]?\s*\)$/); + if (urlMatch) { + asset.src = urlMatch[1]; + } + } + + // transform the url if necessary (skip for local() font sources and data URIs) + if ( + typeof baseURL[asset.type] !== "undefined" && + typeof asset.src === "string" && + !asset.src.startsWith("local(") && + !asset.src.startsWith("data:") + ) { asset.src = baseURL[asset.type] + asset.src; } diff --git a/packages/melonjs/src/loader/parsers/fontface.js b/packages/melonjs/src/loader/parsers/fontface.js index 4284ec08e..8b554375f 100644 --- a/packages/melonjs/src/loader/parsers/fontface.js +++ b/packages/melonjs/src/loader/parsers/fontface.js @@ -1,4 +1,3 @@ -import { isDataUrl } from "../../utils/string.ts"; import { fontList } from "../cache.js"; /** @@ -10,7 +9,7 @@ import { fontList } from "../cache.js"; * @ignore * @example * preloadFontFace( - * name: "'kenpixel'", type: "fontface", src: "url('data/font/kenvector_future.woff2')" + * name: "'kenpixel'", type: "fontface", src: "data/font/kenvector_future.woff2" * ]); */ export function preloadFontFace(data, onload, onerror) { @@ -19,11 +18,10 @@ export function preloadFontFace(data, onload, onerror) { ? globalThis.document.fonts : undefined; - if (isDataUrl(data.src) === true) { - // make sure it in the `url(data:[][;base64],)` format as expected by FontFace - if (!data.src.startsWith("url(")) { - data.src = "url(" + data.src + ")"; - } + // FontFace constructor expects src in `url(...)` or `local()` format + // only wrap plain paths in url(); leave url(), local(), and data URIs as-is + if (!data.src.startsWith("url(") && !data.src.startsWith("local(")) { + data.src = "url(" + data.src + ")"; } if (typeof fontFaceSet !== "undefined") { diff --git a/packages/melonjs/tests/loader.spec.js b/packages/melonjs/tests/loader.spec.js index 119f82ef1..b6a17c1a7 100644 --- a/packages/melonjs/tests/loader.spec.js +++ b/packages/melonjs/tests/loader.spec.js @@ -264,11 +264,71 @@ describe("loader", () => { expect(loader.baseURL["json"]).toBe("https://cdn.example.com/"); expect(loader.baseURL["binary"]).toBe("https://cdn.example.com/"); expect(loader.baseURL["tmx"]).toBe("https://cdn.example.com/"); + expect(loader.baseURL["fontface"]).toBe("https://cdn.example.com/"); // reset loader.setBaseURL("*", "./"); }); + it("should strip url() wrapper from fontface src before applying baseURL", () => { + loader.setBaseURL("fontface", "assets/"); + const receivedSrc = []; + + // stub fontface parser to capture the resolved src + loader.setParser("fontface", (data, onload) => { + receivedSrc.push(data.src); + if (typeof onload === "function") { + onload(); + } + return 1; + }); + + // plain path + loader.load( + { name: "font1", type: "fontface", src: "font/test.woff2" }, + () => {}, + ); + // url() wrapped + loader.load( + { name: "font2", type: "fontface", src: "url(font/test.woff2)" }, + () => {}, + ); + // url() with quotes + loader.load( + { name: "font3", type: "fontface", src: "url('font/test.woff2')" }, + () => {}, + ); + + // all should resolve to the same baseURL + path + expect(receivedSrc[0]).toBe("assets/font/test.woff2"); + expect(receivedSrc[1]).toBe("assets/font/test.woff2"); + expect(receivedSrc[2]).toBe("assets/font/test.woff2"); + + // reset + loader.setBaseURL("fontface", "./"); + }); + + it("should not strip local() wrapper from fontface src", () => { + // reset baseURL so it doesn't interfere + loader.setBaseURL("fontface", "./"); + const receivedSrc = []; + + loader.setParser("fontface", (data, onload) => { + receivedSrc.push(data.src); + if (typeof onload === "function") { + onload(); + } + return 1; + }); + + loader.load( + { name: "font4", type: "fontface", src: "local('My Font')" }, + () => {}, + ); + + expect(receivedSrc[0]).toBe("local('My Font')"); + }); + it("should configure loader options", () => { loader.setOptions({ crossOrigin: "use-credentials",