From 09b9383a0bba3e4b6caf944a57f5e79e1d1b508a Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Tue, 26 Aug 2025 11:34:10 +0200 Subject: [PATCH] :sparkles: Choose closest font weight for token weight when no matching weight is found --- .../data/workspace/tokens/application.cljs | 5 +- frontend/src/app/main/fonts.cljs | 39 ++++++ frontend/test/frontend_tests/fonts_test.cljs | 126 ++++++++++++++++++ 3 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 frontend/test/frontend_tests/fonts_test.cljs diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 13b01d0302..6eb4474b24 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -333,10 +333,7 @@ (txt/is-paragraph-node? node))) update-fn (fn [node _] (let [font (fonts/get-font-data (:font-id node)) - font-variant-id (or - (fonts/find-variant font font-variant) - ;; When variant with matching weight but not with matching style (italic) is found, use that one - (fonts/find-variant font (dissoc font-variant :style)))] + font-variant-id (fonts/find-closest-variant font (:weight font-variant) (:style font-variant))] (if font-variant-id (-> node (d/txt-merge (assoc font-variant :font-variant-id (:id font-variant-id))) diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 81fc8c211a..247d53c3c7 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -270,6 +270,45 @@ (let [props (keys variant-data)] (d/seek #(= (select-keys % props) variant-data) variants))) +(defn find-closest-variant + "Find the closest font weight variant in `font` for `target-weight` with optional `target-style` match. + When exactly between two weights, choose the higher one." + [font target-weight target-style] + (when-let [target-weight (d/parse-integer target-weight)] + (let [variants (:variants font []) + result + (reduce + (fn [closest-match variant] + (let [weight (d/parse-integer (:weight variant)) + distance (abs (- target-weight weight)) + matches-style? (= target-style (:style variant)) + current {:variant variant + :weight weight + :distance distance}] + (cond + ;; Exact match found + (and (zero? distance) + (if target-style matches-style? true)) + (reduced current) + + (nil? closest-match) current + + ;; Update best match if this variant is closer or equal distance but higher weight + (or (< distance (:distance closest-match)) + (and (= distance (:distance closest-match)) + (> weight (:weight closest-match)))) + current + + ;; Same weight as the `closest-match` but the style matches `target-style` + (and (= weight (:weight closest-match)) matches-style?) + current + + :else + closest-match))) + nil + variants)] + (:variant result)))) + ;; Font embedding functions (defn get-node-fonts "Extracts the fonts used by some node" diff --git a/frontend/test/frontend_tests/fonts_test.cljs b/frontend/test/frontend_tests/fonts_test.cljs new file mode 100644 index 0000000000..a59f82a1bf --- /dev/null +++ b/frontend/test/frontend_tests/fonts_test.cljs @@ -0,0 +1,126 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.fonts-test + (:require + [app.main.fonts :as fonts] + [cljs.test :as t :include-macros true])) + +(def sample-font + {:id "sourcesanspro" + :name "Source Sans Pro" + :family "sourcesanspro" + :variants + [{:id "200" + :name "200" + :weight "200" + :style "normal" + :suffix "extralight" + :ttf-url "sourcesanspro-extralight.ttf"} + {:id "200italic" + :name "200 Italic" + :weight "200" + :style "italic" + :suffix "extralightitalic" + :ttf-url "sourcesanspro-extralightitalic.ttf"} + {:id "300" + :name "300" + :weight "300" + :style "normal" + :suffix "light" + :ttf-url "sourcesanspro-light.ttf"} + {:id "300italic" + :name "300 Italic" + :weight "300" + :style "italic" + :suffix "lightitalic" + :ttf-url "sourcesanspro-lightitalic.ttf"} + {:id "regular" + :name "400" + :weight "400" + :style "normal" + :ttf-url "sourcesanspro-regular.ttf"} + {:id "italic" + :name "400 Italic" + :weight "400" + :style "italic" + :ttf-url "sourcesanspro-italic.ttf"} + {:id "bold" + :name "700" + :weight "700" + :style "normal" + :ttf-url "sourcesanspro-bold.ttf"} + {:id "bolditalic" + :name "700 Italic" + :weight "700" + :style "italic" + :ttf-url "sourcesanspro-bolditalic.ttf"} + {:id "black" + :name "900" + :weight "900" + :style "normal" + :ttf-url "sourcesanspro-black.ttf"} + {:id "blackitalic" + :name "900 Italic" + :weight "900" + :style "italic" + :ttf-url "sourcesanspro-blackitalic.ttf"}] + :backend :builtin}) + +(t/deftest find-closest-weight-variant-test + (t/testing "finds exact weight match" + (let [result (fonts/find-closest-variant sample-font "400" nil)] + (t/is (= "400" (:weight result))) + (t/is (= "normal" (:style result))))) + + (t/testing "finds exact weight match with style" + (let [result (fonts/find-closest-variant sample-font "400" "italic")] + (t/is (= "400" (:weight result))) + (t/is (= "italic" (:style result))))) + + (t/testing "chooses higher weight when exactly between two weights" + (let [result (fonts/find-closest-variant sample-font "350" nil)] + (t/is (= "400" (:weight result))))) + + (t/testing "finds exact weight match with style" + (let [result (fonts/find-closest-variant sample-font "350" "italic")] + (t/is (= "400" (:weight result))) + (t/is (= "italic" (:style result))))) + + (t/testing "finds closest weight below minimum available" + (let [result (fonts/find-closest-variant sample-font "0" nil)] + (t/is (= "200" (:weight result))))) + + (t/testing "finds closest weight above maximum available" + (let [result (fonts/find-closest-variant sample-font "1000" nil)] + (t/is (= "900" (:weight result))))) + + (t/testing "keeps the closest weight match when style is not found" + (let [font {:id "sourcesanspro" + :name "Source Sans Pro" + :family "sourcesanspro" + :variants + [{:id "200italic" + :name "200 Italic" + :weight "200" + :style "italic" + :suffix "extralightitalic" + :ttf-url "sourcesanspro-extralightitalic.ttf"} + {:id "300" + :name "300" + :weight "300" + :style "normal" + :suffix "light" + :ttf-url "sourcesanspro-light.ttf"} + {:id "300italic" + :name "300 Italic" + :weight "300" + :style "italic" + :suffix "lightitalic" + :ttf-url "sourcesanspro-lightitalic.ttf"}]} + result (fonts/find-closest-variant font "200" nil)] + (t/is (= "200" (:weight result))) + (t/is (= "italic" (:style result))))))