diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc index 81791d652..93dc8ec04 100644 --- a/common/app/common/pages.cljc +++ b/common/app/common/pages.cljc @@ -55,6 +55,36 @@ (>= % min-safe-int) (<= % max-safe-int))) + + +(s/def :internal.gradient.stop/color ::string) +(s/def :internal.gradient.stop/opacity ::safe-number) +(s/def :internal.gradient.stop/offset ::safe-number) + +(s/def :internal.gradient/type #{:linear :radial}) +(s/def :internal.gradient/start-x ::safe-number) +(s/def :internal.gradient/start-y ::safe-number) +(s/def :internal.gradient/end-x ::safe-number) +(s/def :internal.gradient/end-y ::safe-number) +(s/def :internal.gradient/width ::safe-number) + +(s/def :internal.gradient/stop + (s/keys :req-un [:internal.gradient.stop/color + :internal.gradient.stop/opacity + :internal.gradient.stop/offset])) + +(s/def :internal.gradient/stops + (s/coll-of :internal.gradient/stop :kind vector?)) + +(s/def ::gradient + (s/keys :req-un [:internal.gradient/type + :internal.gradient/start-x + :internal.gradient/start-y + :internal.gradient/end-x + :internal.gradient/end-y + :internal.gradient/width + :internal.gradient/stops])) + ;; Page Options (s/def :internal.page.grid.color/value string?) (s/def :internal.page.grid.color/opacity ::safe-number) @@ -110,10 +140,13 @@ (s/def :internal.shape/blocked boolean?) (s/def :internal.shape/collapsed boolean?) (s/def :internal.shape/content any?) + (s/def :internal.shape/fill-color string?) +(s/def :internal.shape/fill-opacity ::safe-number) +(s/def :internal.shape/fill-gradient (s/nilable ::gradient)) (s/def :internal.shape/fill-color-ref-file (s/nilable uuid?)) (s/def :internal.shape/fill-color-ref-id (s/nilable uuid?)) -(s/def :internal.shape/fill-opacity ::safe-number) + (s/def :internal.shape/font-family string?) (s/def :internal.shape/font-size ::safe-integer) (s/def :internal.shape/font-style string?) @@ -262,13 +295,26 @@ :internal.page/options :internal.page/objects])) + (s/def :internal.color/name ::string) -(s/def :internal.color/value ::string) +(s/def :internal.color/value (s/nilable ::string)) +(s/def :internal.color/color (s/nilable ::string)) +(s/def :internal.color/opacity (s/nilable ::safe-number)) +(s/def :internal.color/gradient (s/nilable ::gradient)) (s/def ::color (s/keys :req-un [::id - :internal.color/name - :internal.color/value])) + :internal.color/name] + :opt-un [:internal.color/value + :internal.color/color + :internal.color/opacity + :internal.color/gradient])) + +(s/def ::recent-color + (s/keys :opt-un [:internal.color/value + :internal.color/color + :internal.color/opacity + :internal.color/gradient])) (s/def :internal.media-object/name ::string) (s/def :internal.media-object/path ::string) @@ -294,7 +340,7 @@ (s/map-of ::uuid ::color)) (s/def :internal.file/recent-colors - (s/coll-of ::string :kind vector?)) + (s/coll-of ::recent-color :kind vector?)) (s/def :internal.typography/id ::id) (s/def :internal.typography/name ::string) @@ -408,8 +454,10 @@ (defmethod change-spec :del-color [_] (s/keys :req-un [::id])) +(s/def :internal.changes.add-recent-color/color ::recent-color) + (defmethod change-spec :add-recent-color [_] - (s/keys :req-un [:recent-color/color])) + (s/keys :req-un [:internal.changes.add-recent-color/color])) (s/def :internal.changes.media/object ::media-object) @@ -821,7 +869,7 @@ (defmethod process-change :mod-color [data {:keys [color]}] - (d/update-in-when data [:colors (:id color)] merge color)) + (d/assoc-in-when data [:colors (:id color)] color)) (defmethod process-change :del-color [data {:keys [id]}] @@ -917,3 +965,4 @@ (ex/raise :type :not-implemented :code :operation-not-implemented :context {:type (:type op)})) + diff --git a/frontend/resources/images/icons/picker-ramp.svg b/frontend/resources/images/icons/picker-ramp.svg index 0e078a017..815b3a944 100644 --- a/frontend/resources/images/icons/picker-ramp.svg +++ b/frontend/resources/images/icons/picker-ramp.svg @@ -1 +1,3 @@ - + + + diff --git a/frontend/resources/images/icons/picker.svg b/frontend/resources/images/icons/picker.svg index be86a1808..3fc711f38 100644 --- a/frontend/resources/images/icons/picker.svg +++ b/frontend/resources/images/icons/picker.svg @@ -1,4 +1,3 @@ - diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 129b1f618..589aef343 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -18,7 +18,7 @@ } }, "auth.create-demo-profile" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:133", "src/app/main/ui/auth/register.cljs:136", "src/app/main/ui/auth/login.cljs:144", "src/app/main/ui/auth/login.cljs:147" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:144", "src/app/main/ui/auth/login.cljs:147", "src/app/main/ui/auth/register.cljs:133", "src/app/main/ui/auth/register.cljs:136" ], "translations" : { "en" : "Just wanna try it?", "fr" : "Vous voulez juste essayer?", @@ -36,7 +36,7 @@ } }, "auth.email" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:101", "src/app/main/ui/auth/recovery_request.cljs:47", "src/app/main/ui/auth/login.cljs:92" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:92", "src/app/main/ui/auth/register.cljs:101", "src/app/main/ui/auth/recovery_request.cljs:47" ], "translations" : { "en" : "Email", "fr" : "Adresse email", @@ -177,7 +177,7 @@ } }, "auth.password" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:106", "src/app/main/ui/auth/login.cljs:99" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:99", "src/app/main/ui/auth/register.cljs:106" ], "translations" : { "en" : "Password", "fr" : "Mot de passe", @@ -276,7 +276,7 @@ } }, "dashboard.add-shared" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:221", "src/app/main/ui/dashboard/grid.cljs:177" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:224", "src/app/main/ui/dashboard/grid.cljs:180" ], "translations" : { "en" : "Add as Shared Library", "fr" : "", @@ -322,7 +322,7 @@ } }, "dashboard.empty-files" : { - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:184" ], + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:187" ], "translations" : { "en" : "You still have no files here", "fr" : "Vous n'avez encore aucun fichier ici", @@ -533,7 +533,7 @@ } }, "dashboard.remove-shared" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:219", "src/app/main/ui/dashboard/grid.cljs:176" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:222", "src/app/main/ui/dashboard/grid.cljs:179" ], "translations" : { "en" : "Remove as Shared Library", "fr" : "", @@ -578,7 +578,7 @@ } }, "dashboard.show-all-files" : { - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:246" ], + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:249" ], "translations" : { "en" : "Show all files", "es" : "Ver todos los ficheros" @@ -636,7 +636,7 @@ } }, "dashboard.update-settings" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:80", "src/app/main/ui/settings/password.cljs:96", "src/app/main/ui/settings/options.cljs:72" ], + "used-in" : [ "src/app/main/ui/settings/options.cljs:72", "src/app/main/ui/settings/profile.cljs:80", "src/app/main/ui/settings/password.cljs:96" ], "translations" : { "en" : "Update settings", "fr" : "Mettre à jour les paramètres", @@ -645,7 +645,7 @@ } }, "dashboard.your-account-title" : { - "used-in" : [ "src/app/main/ui/settings.cljs:28" ], + "used-in" : [ "src/app/main/ui/settings.cljs:29" ], "translations" : { "en" : "Your account", "es" : "Su cuenta" @@ -712,7 +712,7 @@ "unused" : true }, "ds.confirm-cancel" : { - "used-in" : [ "src/app/main/ui/confirm.cljs:28" ], + "used-in" : [ "src/app/main/ui/confirm.cljs:36" ], "translations" : { "en" : "Cancel", "fr" : "Annuler", @@ -721,7 +721,7 @@ } }, "ds.confirm-ok" : { - "used-in" : [ "src/app/main/ui/confirm.cljs:29" ], + "used-in" : [ "src/app/main/ui/confirm.cljs:37" ], "translations" : { "en" : "Ok", "fr" : "Ok", @@ -730,7 +730,7 @@ } }, "ds.confirm-title" : { - "used-in" : [ "src/app/main/ui/confirm.cljs:27", "src/app/main/ui/confirm.cljs:30" ], + "used-in" : [ "src/app/main/ui/confirm.cljs:35", "src/app/main/ui/confirm.cljs:39" ], "translations" : { "en" : "Are you sure?", "fr" : "Êtes-vous sûr?", @@ -757,7 +757,7 @@ } }, "errors.email-already-exists" : { - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:80", "src/app/main/ui/settings/change_email.cljs:47" ], + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:47", "src/app/main/ui/auth/verify_token.cljs:80" ], "translations" : { "en" : "Email already used", "fr" : "Adresse e-mail déjà utilisée", @@ -784,7 +784,7 @@ } }, "errors.generic" : { - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:89", "src/app/main/ui/settings/profile.cljs:40", "src/app/main/ui/settings/options.cljs:32" ], + "used-in" : [ "src/app/main/ui/settings/options.cljs:32", "src/app/main/ui/settings/profile.cljs:40", "src/app/main/ui/auth/verify_token.cljs:89" ], "translations" : { "en" : "Something wrong has happened.", "fr" : "Quelque chose c'est mal passé.", @@ -811,7 +811,7 @@ } }, "errors.media-type-mismatch" : { - "used-in" : [ "src/app/main/data/workspace/persistence.cljs:413", "src/app/main/data/media.cljs:61" ], + "used-in" : [ "src/app/main/data/media.cljs:61", "src/app/main/data/workspace/persistence.cljs:413" ], "translations" : { "en" : "Seems that the contents of the image does not match the file extension.", "fr" : "", @@ -820,7 +820,7 @@ } }, "errors.media-type-not-allowed" : { - "used-in" : [ "src/app/main/data/workspace/persistence.cljs:410", "src/app/main/data/media.cljs:58" ], + "used-in" : [ "src/app/main/data/media.cljs:58", "src/app/main/data/workspace/persistence.cljs:410" ], "translations" : { "en" : "Seems that this is not a valid image.", "fr" : "", @@ -865,7 +865,7 @@ } }, "errors.unexpected-error" : { - "used-in" : [ "src/app/main/data/media.cljs:64", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66", "src/app/main/ui/auth/register.cljs:45" ], + "used-in" : [ "src/app/main/data/media.cljs:64", "src/app/main/ui/auth/register.cljs:45", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66" ], "translations" : { "en" : "An unexpected error occurred.", "fr" : "Une erreur inattendue c'est produite", @@ -931,7 +931,7 @@ } }, "labels.delete" : { - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:174", "src/app/main/ui/dashboard/files.cljs:85" ], + "used-in" : [ "src/app/main/ui/dashboard/files.cljs:85", "src/app/main/ui/dashboard/grid.cljs:177" ], "translations" : { "en" : "Delete", "fr" : "Supprimer", @@ -973,7 +973,7 @@ } }, "labels.logout" : { - "used-in" : [ "src/app/main/ui/settings.cljs:30", "src/app/main/ui/dashboard/sidebar.cljs:459" ], + "used-in" : [ "src/app/main/ui/settings.cljs:31", "src/app/main/ui/dashboard/sidebar.cljs:459" ], "translations" : { "en" : "Logout", "fr" : "Quitter", @@ -982,7 +982,7 @@ } }, "labels.members" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:59", "src/app/main/ui/dashboard/team.cljs:63", "src/app/main/ui/dashboard/sidebar.cljs:295" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:295", "src/app/main/ui/dashboard/team.cljs:59", "src/app/main/ui/dashboard/team.cljs:63" ], "translations" : { "en" : "Members" } @@ -1064,7 +1064,7 @@ } }, "labels.rename" : { - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:173", "src/app/main/ui/dashboard/sidebar.cljs:298", "src/app/main/ui/dashboard/files.cljs:84" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:298", "src/app/main/ui/dashboard/files.cljs:84", "src/app/main/ui/dashboard/grid.cljs:176" ], "translations" : { "en" : "Rename", "es" : "Renombrar" @@ -1077,7 +1077,7 @@ } }, "labels.settings" : { - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:80", "src/app/main/ui/dashboard/team.cljs:65", "src/app/main/ui/dashboard/sidebar.cljs:296" ], + "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:80", "src/app/main/ui/dashboard/sidebar.cljs:296", "src/app/main/ui/dashboard/team.cljs:65" ], "translations" : { "en" : "Settings", "fr" : "Settings", @@ -1111,7 +1111,7 @@ } }, "media.loading" : { - "used-in" : [ "src/app/main/data/workspace/persistence.cljs:394", "src/app/main/data/media.cljs:43" ], + "used-in" : [ "src/app/main/data/media.cljs:43", "src/app/main/data/workspace/persistence.cljs:394" ], "translations" : { "en" : "Loading image...", "fr" : "Chargement de l'image...", @@ -1129,7 +1129,7 @@ "unused" : true }, "modals.add-shared-confirm.accept" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:113", "src/app/main/ui/dashboard/grid.cljs:115" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:114", "src/app/main/ui/dashboard/grid.cljs:116" ], "translations" : { "en" : "Add as Shared Library", "fr" : "", @@ -1147,7 +1147,7 @@ } }, "modals.add-shared-confirm.message" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:110", "src/app/main/ui/dashboard/grid.cljs:112" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:111", "src/app/main/ui/dashboard/grid.cljs:113" ], "translations" : { "en" : "Add “%s” as Shared Library", "fr" : "", @@ -1381,7 +1381,7 @@ } }, "modals.remove-shared-confirm.accept" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:124", "src/app/main/ui/dashboard/grid.cljs:129" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:127", "src/app/main/ui/dashboard/grid.cljs:132" ], "translations" : { "en" : "Remove as Shared Library", "fr" : "", @@ -1390,7 +1390,7 @@ } }, "modals.remove-shared-confirm.hint" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:123", "src/app/main/ui/dashboard/grid.cljs:128" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:125", "src/app/main/ui/dashboard/grid.cljs:130" ], "translations" : { "en" : "Once removed as Shared Library, the File Library of this file will stop being available to be used among the rest of your files.", "fr" : "", @@ -1399,7 +1399,7 @@ } }, "modals.remove-shared-confirm.message" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:122", "src/app/main/ui/dashboard/grid.cljs:127" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:124", "src/app/main/ui/dashboard/grid.cljs:129" ], "translations" : { "en" : "Remove “%s” as Shared Library", "fr" : "", @@ -1417,7 +1417,7 @@ } }, "notifications.profile-saved" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:36", "src/app/main/ui/settings/options.cljs:36" ], + "used-in" : [ "src/app/main/ui/settings/options.cljs:36", "src/app/main/ui/settings/profile.cljs:36" ], "translations" : { "en" : "Profile saved successfully!", "fr" : "Profil enregistré avec succès!", @@ -1426,7 +1426,7 @@ } }, "notifications.validation-email-sent" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:54", "src/app/main/ui/settings/change_email.cljs:56" ], + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:56", "src/app/main/ui/auth/register.cljs:54" ], "translations" : { "en" : "Verification email sent to %s; check your email!" } @@ -1441,7 +1441,7 @@ } }, "settings.multiple" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:156", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:145", "src/app/main/ui/workspace/sidebar/options/typography.cljs:99", "src/app/main/ui/workspace/sidebar/options/typography.cljs:149", "src/app/main/ui/workspace/sidebar/options/typography.cljs:162" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:153", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:163", "src/app/main/ui/workspace/sidebar/options/typography.cljs:99", "src/app/main/ui/workspace/sidebar/options/typography.cljs:149", "src/app/main/ui/workspace/sidebar/options/typography.cljs:162", "src/app/main/ui/workspace/sidebar/options/stroke.cljs:156" ], "translations" : { "en" : "Mixed", "fr" : null, @@ -1666,7 +1666,7 @@ } }, "workspace.assets.assets" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:615" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:629" ], "translations" : { "en" : "Assets", "fr" : "", @@ -1675,7 +1675,7 @@ } }, "workspace.assets.box-filter-all" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:635" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:649" ], "translations" : { "en" : "All assets", "fr" : "", @@ -1702,7 +1702,7 @@ "unused" : true }, "workspace.assets.colors" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:320", "src/app/main/ui/workspace/sidebar/assets.cljs:638" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:329", "src/app/main/ui/workspace/sidebar/assets.cljs:652" ], "translations" : { "en" : "Colors", "fr" : "", @@ -1711,7 +1711,7 @@ } }, "workspace.assets.components" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:82", "src/app/main/ui/workspace/sidebar/assets.cljs:636" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:83", "src/app/main/ui/workspace/sidebar/assets.cljs:650" ], "translations" : { "en" : "Components", "fr" : "", @@ -1720,7 +1720,7 @@ } }, "workspace.assets.delete" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:102", "src/app/main/ui/workspace/sidebar/assets.cljs:190", "src/app/main/ui/workspace/sidebar/assets.cljs:296", "src/app/main/ui/workspace/sidebar/assets.cljs:419" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:103", "src/app/main/ui/workspace/sidebar/assets.cljs:191", "src/app/main/ui/workspace/sidebar/assets.cljs:305", "src/app/main/ui/workspace/sidebar/assets.cljs:433" ], "translations" : { "en" : "Delete", "fr" : "", @@ -1729,7 +1729,7 @@ } }, "workspace.assets.edit" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:295", "src/app/main/ui/workspace/sidebar/assets.cljs:418" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:304", "src/app/main/ui/workspace/sidebar/assets.cljs:432" ], "translations" : { "en" : "Edit", "fr" : "", @@ -1738,7 +1738,7 @@ } }, "workspace.assets.file-library" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:518" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:532" ], "translations" : { "en" : "File library", "fr" : "", @@ -1747,7 +1747,7 @@ } }, "workspace.assets.graphics" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:163", "src/app/main/ui/workspace/sidebar/assets.cljs:637" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:164", "src/app/main/ui/workspace/sidebar/assets.cljs:651" ], "translations" : { "en" : "Graphics", "fr" : "", @@ -1756,7 +1756,7 @@ } }, "workspace.assets.libraries" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:618" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:632" ], "translations" : { "en" : "Libraries", "fr" : "", @@ -1765,7 +1765,7 @@ } }, "workspace.assets.not-found" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:579" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:593" ], "translations" : { "en" : "No assets found", "fr" : "", @@ -1774,7 +1774,7 @@ } }, "workspace.assets.rename" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:294", "src/app/main/ui/workspace/sidebar/assets.cljs:417" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:303", "src/app/main/ui/workspace/sidebar/assets.cljs:431" ], "translations" : { "en" : "Rename", "fr" : "", @@ -1783,7 +1783,7 @@ } }, "workspace.assets.search" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:622" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:636" ], "translations" : { "en" : "Search assets", "fr" : "", @@ -1792,7 +1792,7 @@ } }, "workspace.assets.shared" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:520" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:534" ], "translations" : { "en" : "SHARED", "fr" : "", @@ -1801,7 +1801,7 @@ } }, "workspace.assets.typography" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:406", "src/app/main/ui/workspace/sidebar/assets.cljs:639" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:420", "src/app/main/ui/workspace/sidebar/assets.cljs:653" ], "translations" : { "en" : "Typographies" } @@ -1854,8 +1854,20 @@ "en" : "Text Transform" } }, + "workspace.gradients.linear" : { + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:39", "src/app/main/ui/components/color_bullet.cljs:30" ], + "translations" : { + "en" : "Linear gradient" + } + }, + "workspace.gradients.radial" : { + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:40", "src/app/main/ui/components/color_bullet.cljs:31" ], + "translations" : { + "en" : "Radial gradient" + } + }, "workspace.header.menu.disable-dynamic-alignment" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:213" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:216" ], "translations" : { "en" : "Disable dynamic alignment", "fr" : "Désactiver l'alignement dynamique", @@ -1864,7 +1876,7 @@ } }, "workspace.header.menu.disable-snap-grid" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:185" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:188" ], "translations" : { "en" : "Disable snap to grid", "fr" : "Désactiver l'alignement sur la grille", @@ -1873,7 +1885,7 @@ } }, "workspace.header.menu.enable-dynamic-alignment" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:214" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:217" ], "translations" : { "en" : "Enable dynamic aligment", "fr" : "Activer l'alignement dynamique", @@ -1882,7 +1894,7 @@ } }, "workspace.header.menu.enable-snap-grid" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:186" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:189" ], "translations" : { "en" : "Snap to grid", "fr" : "Aligner sur la grille", @@ -1891,7 +1903,7 @@ } }, "workspace.header.menu.hide-assets" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:206" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:209" ], "translations" : { "en" : "Hide assets", "fr" : "", @@ -1900,7 +1912,7 @@ } }, "workspace.header.menu.hide-grid" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:178" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:181" ], "translations" : { "en" : "Hide grids", "fr" : "Masquer la grille", @@ -1909,7 +1921,7 @@ } }, "workspace.header.menu.hide-layers" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:192" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:195" ], "translations" : { "en" : "Hide layers", "fr" : "Masquer les couches", @@ -1918,7 +1930,7 @@ } }, "workspace.header.menu.hide-palette" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:199" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:202" ], "translations" : { "en" : "Hide color palette", "fr" : "Masquer la palette de couleurs", @@ -1927,7 +1939,7 @@ } }, "workspace.header.menu.hide-rules" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:171" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:174" ], "translations" : { "en" : "Hide rules", "fr" : "Masquer les règles", @@ -1936,7 +1948,7 @@ } }, "workspace.header.menu.show-assets" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:207" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:210" ], "translations" : { "en" : "Show assets", "fr" : "", @@ -1945,7 +1957,7 @@ } }, "workspace.header.menu.show-grid" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:179" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:182" ], "translations" : { "en" : "Show grid", "fr" : "Montrer la grille", @@ -1954,7 +1966,7 @@ } }, "workspace.header.menu.show-layers" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:193" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:196" ], "translations" : { "en" : "Show layers", "fr" : "Montrer les couches", @@ -1963,7 +1975,7 @@ } }, "workspace.header.menu.show-palette" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:200" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:203" ], "translations" : { "en" : "Show color palette", "fr" : "Montrer la palette de couleurs", @@ -1972,7 +1984,7 @@ } }, "workspace.header.menu.show-rules" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:172" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:175" ], "translations" : { "en" : "Show rules", "fr" : "Montrer les règles", @@ -2005,7 +2017,7 @@ } }, "workspace.header.viewer" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:260" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:263" ], "translations" : { "en" : "View mode (Ctrl + P)", "fr" : "Mode visualisation (Ctrl + P)", @@ -2038,19 +2050,19 @@ } }, "workspace.libraries.colors.file-library" : { - "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:340", "src/app/main/ui/workspace/colorpalette.cljs:149" ], + "used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs:87", "src/app/main/ui/workspace/colorpalette.cljs:149" ], "translations" : { "en" : "File library" } }, "workspace.libraries.colors.recent-colors" : { - "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:339", "src/app/main/ui/workspace/colorpalette.cljs:159" ], + "used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs:86", "src/app/main/ui/workspace/colorpalette.cljs:159" ], "translations" : { "en" : "Recent colors" } }, "workspace.libraries.colors.save-color" : { - "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:375" ], + "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:338" ], "translations" : { "en" : "Save color" } @@ -2290,7 +2302,7 @@ } }, "workspace.options.fill" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:51" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:54" ], "translations" : { "en" : "Fill", "fr" : "Remplissage", @@ -2308,7 +2320,7 @@ } }, "workspace.options.grid.column" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:129" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:132" ], "translations" : { "en" : "Columns", "fr" : "Colonnes", @@ -2317,7 +2329,7 @@ } }, "workspace.options.grid.params.columns" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:170" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:173" ], "translations" : { "en" : "Columns", "fr" : "Colonnes", @@ -2326,7 +2338,7 @@ } }, "workspace.options.grid.params.gutter" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:203" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:206" ], "translations" : { "en" : "Gutter", "fr" : "Gouttière", @@ -2335,7 +2347,7 @@ } }, "workspace.options.grid.params.height" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:194" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:197" ], "translations" : { "en" : "Height", "fr" : "Hauteur", @@ -2344,7 +2356,7 @@ } }, "workspace.options.grid.params.margin" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:209" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:212" ], "translations" : { "en" : "Margin", "fr" : "Marge", @@ -2353,7 +2365,7 @@ } }, "workspace.options.grid.params.rows" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:161" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:164" ], "translations" : { "en" : "Rows", "fr" : "Lignes", @@ -2362,7 +2374,7 @@ } }, "workspace.options.grid.params.set-default" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:222" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:226" ], "translations" : { "en" : "Set as default", "fr" : "Définir par défaut", @@ -2371,7 +2383,7 @@ } }, "workspace.options.grid.params.size" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:154" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:157" ], "translations" : { "en" : "Size", "fr" : "Taille", @@ -2380,7 +2392,7 @@ } }, "workspace.options.grid.params.type" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:179" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:182" ], "translations" : { "en" : "Type", "fr" : "Type", @@ -2389,7 +2401,7 @@ } }, "workspace.options.grid.params.type.bottom" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:187" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:190" ], "translations" : { "en" : "Bottom", "fr" : "Bas", @@ -2398,7 +2410,7 @@ } }, "workspace.options.grid.params.type.center" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:185" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:188" ], "translations" : { "en" : "Center", "fr" : "Centre", @@ -2407,7 +2419,7 @@ } }, "workspace.options.grid.params.type.left" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:184" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:187" ], "translations" : { "en" : "Left", "fr" : "Gauche", @@ -2416,7 +2428,7 @@ } }, "workspace.options.grid.params.type.right" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:188" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:191" ], "translations" : { "en" : "Right", "fr" : "Droite", @@ -2425,7 +2437,7 @@ } }, "workspace.options.grid.params.type.stretch" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:181" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:184" ], "translations" : { "en" : "Stretch", "fr" : "Étirer", @@ -2434,7 +2446,7 @@ } }, "workspace.options.grid.params.type.top" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:183" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:186" ], "translations" : { "en" : "Top", "fr" : "Haut", @@ -2443,7 +2455,7 @@ } }, "workspace.options.grid.params.use-default" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:220" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:224" ], "translations" : { "en" : "Use default", "fr" : "Utiliser la valeur par défaut", @@ -2452,7 +2464,7 @@ } }, "workspace.options.grid.params.width" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:195" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:198" ], "translations" : { "en" : "Width", "fr" : "Largeur", @@ -2461,7 +2473,7 @@ } }, "workspace.options.grid.row" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:130" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:133" ], "translations" : { "en" : "Rows", "fr" : "Lignes", @@ -2470,7 +2482,7 @@ } }, "workspace.options.grid.square" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:128" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:131" ], "translations" : { "en" : "Square", "fr" : "Carré", @@ -2479,7 +2491,7 @@ } }, "workspace.options.grid.title" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:234" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame_grid.cljs:238" ], "translations" : { "en" : "Grid & Layouts", "fr" : "Grille & couches", @@ -2488,7 +2500,7 @@ } }, "workspace.options.group-fill" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:50" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:53" ], "translations" : { "en" : "Group fill", "fr" : null, @@ -2497,7 +2509,7 @@ } }, "workspace.options.group-stroke" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:70" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:72" ], "translations" : { "en" : "Group stroke", "fr" : null, @@ -2524,7 +2536,7 @@ } }, "workspace.options.position" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs:146", "src/app/main/ui/workspace/sidebar/options/frame.cljs:126" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame.cljs:126", "src/app/main/ui/workspace/sidebar/options/measures.cljs:146" ], "translations" : { "en" : "Position", "fr" : "Position", @@ -2578,7 +2590,7 @@ } }, "workspace.options.selection-fill" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:49" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:52" ], "translations" : { "en" : "Selection fill", "fr" : null, @@ -2587,7 +2599,7 @@ } }, "workspace.options.selection-stroke" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:69" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:71" ], "translations" : { "en" : "Selection stroke", "fr" : null, @@ -2632,13 +2644,13 @@ } }, "workspace.options.shadow-options.title" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:190" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:194" ], "translations" : { "en" : "Shadow" } }, "workspace.options.size" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs:116", "src/app/main/ui/workspace/sidebar/options/frame.cljs:99" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame.cljs:99", "src/app/main/ui/workspace/sidebar/options/measures.cljs:116" ], "translations" : { "en" : "Size", "fr" : "Taille", @@ -2656,7 +2668,7 @@ } }, "workspace.options.stroke" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:71" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:73" ], "translations" : { "en" : "Stroke", "fr" : "Bordure", @@ -2845,7 +2857,7 @@ } }, "workspace.options.text-options.none" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:153", "src/app/main/ui/workspace/sidebar/options/typography.cljs:178" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:178", "src/app/main/ui/workspace/sidebar/options/text.cljs:153" ], "translations" : { "en" : "None", "fr" : "Aucune", @@ -2960,7 +2972,7 @@ } }, "workspace.sitemap" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:146" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:149" ], "translations" : { "en" : "Sitemap", "fr" : null, @@ -3059,7 +3071,7 @@ } }, "workspace.updates.dismiss" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:521" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:538" ], "translations" : { "en" : "Dismiss", "fr" : "", @@ -3068,7 +3080,7 @@ } }, "workspace.updates.there-are-updates" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:517" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:534" ], "translations" : { "en" : "There are updates in shared libraries", "fr" : "", @@ -3077,7 +3089,7 @@ } }, "workspace.updates.update" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:519" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:536" ], "translations" : { "en" : "Update", "fr" : "", diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 134d14d65..cef562e72 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -78,3 +78,4 @@ @import 'main/partials/user-settings'; @import 'main/partials/workspace'; @import 'main/partials/workspace-header'; +@import 'main/partials/color-bullet'; diff --git a/frontend/resources/styles/main/partials/color-bullet.scss b/frontend/resources/styles/main/partials/color-bullet.scss new file mode 100644 index 000000000..abde93a40 --- /dev/null +++ b/frontend/resources/styles/main/partials/color-bullet.scss @@ -0,0 +1,180 @@ +// 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/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL + +.color-cell { + .color-bullet { + background-color: $color-white; + // Creates strange artifacts + border: 2px solid $color-gray-60; + // box-shadow: 0 0 0 2px $color-gray-60; + border-radius: 50%; + } + + &.cell-big .color-bullet { + width: 50px; + height: 50px; + } + + &.cell-small .color-bullet { + width: 40px; + height: 40px; + } + + .color-bullet.color-big { + width: 50px; + height: 50px; + } + +} + +.color-cell.current { + .color-bullet { + border-color: $color-gray-50; + } +} + +ul.palette-menu .color-bullet { + width: 20px; + height: 20px; + border-radius: 12px; + border: 1px solid $color-gray-10; + margin-right: 5px; + background-size: 8px; +} +.color-cell.add-color .color-bullet { + align-items: center; + background-color: $color-gray-50; + border: 3px dashed $color-gray-10; + cursor: pointer; + display: flex; + justify-content: center; + margin-bottom: 1rem; + padding: .6rem; + + svg { + fill: $color-gray-10; + height: 30px; + width: 30px; + } +} + +.colorpicker-content .color-bullet { + grid-area: color; + width: 20px; + height: 20px; + border-radius: 12px; + border: 1px solid $color-gray-10; + background-size: 8px; +} + +.asset-group .group-list-item .color-bullet { + width: 20px; + height: 20px; + border-radius: 10px; + margin-right: $x-small; +} + +.color-cell.add-color:hover .color-bullet { + border-color: $color-gray-30; + + svg { + fill: $color-gray-30; + } +} + +.color-bullet { + display: flex; + flex-direction: row; + overflow: hidden; + + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") left center; + background-color: $color-white; + + & > * { + width: 100%; + height: 100%; + } +} + +.color-data .color-bullet.multiple { + background: transparent; + + &::before { + content: "?" + } +} + +.color-data .color-bullet { + background-color: $color-gray-30; + border: 1px solid $color-gray-30; + border-radius: $br-small; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: $color-gray-10; + flex-shrink: 0; + height: 20px; + margin: 5px 4px 0 0; + width: 20px; + + &.color-name { + border-radius: 10px; + } + + &.palette-th { + align-items: center; + border: 1px solid $color-gray-30; + display: flex; + justify-content: center; + + svg { + fill: $color-gray-30; + height: 16px; + width: 16px; + } + + &:hover { + border-color: $color-primary; + svg { + fill: $color-primary; + } + + } + } +} + +.colorpicker-content .libraries .selected-colors .color-bullet { + grid-area: auto; + margin-bottom: 0.25rem; + cursor: pointer; + + &:hover { + border-color: $color-primary; + } + + &.button { + background: $color-white; + display: flex; + align-items: center; + justify-content: center; + } + + &.button svg { + width: 12px; + height: 12px; + fill: $color-gray-30; + } + + &.plus-button svg { + width: 8px; + height: 8px; + fill: $color-black; + } +} diff --git a/frontend/resources/styles/main/partials/color-palette.scss b/frontend/resources/styles/main/partials/color-palette.scss index c4a702eee..7bd2ab47f 100644 --- a/frontend/resources/styles/main/partials/color-palette.scss +++ b/frontend/resources/styles/main/partials/color-palette.scss @@ -123,7 +123,7 @@ display: flex; overflow: hidden; width: 100%; - height: 4.8rem; + height: 5rem; padding: 0.25rem; &.size-small { @@ -156,28 +156,6 @@ flex-basis: 52px; } - .color { - background-color: $color-gray-10; - border: 2px solid $color-gray-60; - border-radius: 50%; - flex-shrink: 0; - } - - &.cell-big .color { - width: 50px; - height: 50px; - } - - &.cell-small .color { - width: 40px; - height: 40px; - } - - .color.color-big { - width: 50px; - height: 50px; - } - .color-text { color: $color-gray-20; font-size: $fs12; @@ -186,11 +164,9 @@ text-overflow: ellipsis; width: 66px; text-align: center; + margin-top: 0.25rem; } &.current { - .color { - border-color: $color-gray-50; - } .color-text { color: $color-gray-50; font-weight: bold; @@ -217,31 +193,11 @@ } &.add-color { margin-left: 1.5rem; - .color { - align-items: center; - background-color: $color-gray-50; - border: 3px dashed $color-gray-10; - cursor: pointer; - display: flex; - justify-content: center; - margin-bottom: 1rem; - padding: .6rem; - svg { - fill: $color-gray-10; - height: 30px; - width: 30px; - } - } + .color-text { font-weight: bold; } &:hover { - .color { - border-color: $color-gray-30; - svg { - fill: $color-gray-30; - } - } .color-text { color: $color-gray-40; } @@ -336,12 +292,5 @@ ul.palette-menu { margin-top: 0.5rem; } - .color-bullet { - width: 20px; - height: 20px; - border-radius: 12px; - border: 1px solid $color-gray-10; - margin-right: 5px; - } } diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index 9b3d268e4..70a43f3b3 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -29,7 +29,7 @@ border: none; cursor: pointer; - &.active, + &.active svg, &:hover svg { fill: $color-primary; } @@ -72,10 +72,16 @@ margin-top: 0.5rem; margin-bottom: 1rem; - .gradient-background { + .gradient-background-wrapper { height: 100%; width: 100%; border: 1px solid $color-gray-10; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") left center; + } + + .gradient-background { + height: 100%; + width: 100%; } .gradient-stop-wrapper { @@ -85,16 +91,21 @@ } .gradient-stop { + display: grid; + grid-template-columns: 50% 50%; position: absolute; - width: 14px; - height: 14px; + width: 15px; + height: 15px; border-radius: 2px; border: 1px solid $color-gray-20; margin-top: -2px; margin-left: -7px; box-shadow: 0 2px 2px rgb(0 0 0 / 15%); - .selected { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") left center; + background-color: $color-white; + + &.active { border-color: $color-primary; } } @@ -230,15 +241,6 @@ } } - .color-bullet { - grid-area: color; - width: 20px; - height: 20px; - background-color: rgba(var(--color)); - border-radius: 12px; - border: 1px solid $color-gray-10; - } - .shade-selector { display: grid; justify-items: center; @@ -269,6 +271,10 @@ justify-items: center; grid-column-gap: 0.25rem; + &.disable-opacity { + grid-template-columns: 3.5rem repeat(3, 1fr); + } + input { width: 100%; margin: 0; @@ -325,34 +331,6 @@ content: ""; flex: auto; } - - .selected-colors .color-bullet { - grid-area: auto; - margin-bottom: 0.25rem; - cursor: pointer; - - &:hover { - border-color: $color-primary; - } - - &.button { - display: flex; - align-items: center; - justify-content: center; - } - - &.button svg { - width: 12px; - height: 12px; - fill: $color-gray-30; - } - - &.plus-button svg { - width: 8px; - height: 8px; - fill: $color-black; - } - } } .actions { @@ -465,13 +443,10 @@ .colorpicker-tabs { display: flex; - margin-top: 0.25rem; + margin: 0.5rem 0; + border-radius: 5px; + border: 1px solid $color-gray-10; height: 2rem; - background-color: $color-gray-10; - - .active { - background-color: $color-white; - } .colorpicker-tab { cursor: pointer; @@ -483,9 +458,21 @@ svg { width: 16px; height: 16px; - fill: $color-gray-30; + fill: $color-gray-20; } } + + .active { + background-color: $color-gray-10; + svg { + fill: $color-gray-60; + } + } + + :hover svg { + fill: $color-primary; + } + } } diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index d4d6ff4ed..a99b426fb 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -241,13 +241,6 @@ color: $color-white; cursor: pointer; - & .color-block { - width: 20px; - height: 20px; - border-radius: 10px; - margin-right: $x-small; - } - & span { margin-left: $x-small; color: $color-gray-30; diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 86cd80653..f1aa92e27 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -467,47 +467,6 @@ margin-bottom: 0; } -.color-th { - background-color: $color-gray-30; - border: 1px solid $color-gray-30; - border-radius: $br-small; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - color: $color-gray-10; - flex-shrink: 0; - height: 20px; - margin: 5px 4px 0 0; - width: 20px; - - &.color-name { - border-radius: 10px; - } - - &.palette-th { - align-items: center; - border: 1px solid $color-gray-30; - display: flex; - justify-content: center; - - svg { - fill: $color-gray-30; - height: 16px; - width: 16px; - } - - &:hover { - border-color: $color-primary; - svg { - fill: $color-primary; - } - - } - - } -} - .presets { .custom-select-dropdown { width: 200px; diff --git a/frontend/src/app/main/data/colors.cljs b/frontend/src/app/main/data/colors.cljs index 217f28aad..049452a00 100644 --- a/frontend/src/app/main/data/colors.cljs +++ b/frontend/src/app/main/data/colors.cljs @@ -104,27 +104,26 @@ (assoc-in [:workspace-local :picked-color-select] value) (assoc-in [:workspace-local :picked-shift?] shift?))))) - (defn change-fill - ([ids color id file-id] - (change-fill ids color 1 id file-id)) - ([ids color opacity id file-id] + ([ids color] (ptk/reify ::change-fill ptk/WatchEvent (watch [_ state s] (let [pid (:current-page-id state) objects (get-in state [:workspace-data :pages-index pid :objects]) - children (mapcat #(cph/get-children % objects) ids) + not-frame (fn [shape-id] (not= (get-in objects [shape-id :type]) :frame)) + children (->> ids (filter not-frame) (mapcat #(cph/get-children % objects))) ids (into ids children) is-text? #(= :text (:type (get objects %))) text-ids (filter is-text? ids) shape-ids (filter (comp not is-text?) ids) - attrs (cond-> {:fill-color color - :fill-color-ref-id id - :fill-color-ref-file file-id} - (and opacity (not= opacity :multiple)) (assoc :fill-opacity opacity)) + attrs (cond-> {:fill-color (:color color) + :fill-color-ref-id (:id color) + :fill-color-ref-file (:file-id color) + :fill-color-gradient (:gradient color) + :fill-opacity (:opacity color)}) update-fn (fn [shape] (merge shape attrs)) editors (get-in state [:workspace-local :editors]) @@ -135,20 +134,22 @@ (map #(dwt/update-text-attrs {:id % :editor (get editors %) :attrs attrs}) text-ids) (dwc/update-shapes shape-ids update-fn)))))))) -(defn change-stroke [ids color id file-id] +(defn change-stroke [ids color] (ptk/reify ::change-stroke ptk/WatchEvent (watch [_ state s] (let [objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects]) - children (mapcat #(cph/get-children % objects) ids) + not-frame (fn [shape-id] (not= (get-in objects [shape-id :type]) :frame)) + children (->> ids (filter not-frame) (mapcat #(cph/get-children % objects))) ids (into ids children) update-fn (fn [s] (cond-> s true - (assoc :stroke-color color - :stroke-color-ref-id id - :stroke-color-ref-file file-id) + (assoc :stroke-color (:color color) + :stroke-color-gradient (:gradient color) + :stroke-color-ref-id (:id color) + :stroke-color-ref-file (:file-id color)) (= (:stroke-style s) :none) (assoc :stroke-style :solid @@ -157,20 +158,67 @@ (rx/of (dwc/update-shapes ids update-fn)))))) (defn picker-for-selected-shape [] - ;; TODO: replace st/emit! by a subject push and set that in the WatchEvent - (let [handle-change-color (fn [color opacity id file-id shift?] - (let [ids (get-in @st/state [:workspace-local :selected])] - (st/emit! - (if shift? - (change-stroke ids color nil nil) - (change-fill ids color nil nil)) - (md/hide))))] - (ptk/reify ::start-picker + (let [sub (rx/subject)] + (ptk/reify ::picker-for-selected-shape + ptk/WatchEvent + (watch [_ state stream] + (let [ids (get-in state [:workspace-local :selected]) + stop? (->> stream + (rx/filter (ptk/type? ::stop-picker))) + + update-events (fn [[color shift?]] + (rx/of (if shift? + (change-stroke ids color) + (change-fill ids color)) + (stop-picker)))] + (rx/merge + ;; Stream that updates the stroke/width and stops if `esc` pressed + (->> sub + (rx/take-until stop?) + (rx/flat-map update-events)) + + ;; Hide the modal if the stop event is emitted + (->> stop? + (rx/first) + (rx/map #(md/hide)))))) + ptk/UpdateEvent (update [_ state] + (let [handle-change-color (fn [color shift?] (rx/push! sub [color shift?]))] + (-> state + (assoc-in [:workspace-local :picking-color?] true) + (assoc ::md/modal {:id (random-uuid) + :data {:color "#000000" :opacity 1} + :type :colorpicker + :props {:on-change handle-change-color} + :allow-click-outside true}))))))) + +(defn start-gradient [gradient] + (ptk/reify ::start-gradient + ptk/UpdateEvent + (update [_ state] + (let [id (first (get-in state [:workspace-local :selected]))] (-> state - (assoc-in [:workspace-local :picking-color?] true) - (assoc ::md/modal {:id (random-uuid) - :type :colorpicker - :props {:on-change handle-change-color} - :allow-click-outside true})))))) + (assoc-in [:workspace-local :current-gradient] gradient) + (assoc-in [:workspace-local :current-gradient :shape-id] id)))))) + +(defn stop-gradient [] + (ptk/reify ::stop-gradient + ptk/UpdateEvent + (update [_ state] + (-> state + (update :workspace-local dissoc :current-gradient))))) + +(defn update-gradient [changes] + (ptk/reify ::update-gradient + ptk/UpdateEvent + (update [_ state] + (-> state + (update-in [:workspace-local :current-gradient] merge changes))))) + +(defn select-gradient-stop [spot] + (ptk/reify ::select-gradient-stop + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :editing-stop] spot))))) diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs index 0b12b21f6..cd0a7c2c5 100644 --- a/frontend/src/app/main/data/modal.cljs +++ b/frontend/src/app/main/data/modal.cljs @@ -29,6 +29,14 @@ :type type :props props :allow-click-outside false}))))) +(defn update-props + ([type props] + (ptk/reify ::show-modal + ptk/UpdateEvent + (update [_ state] + (cond-> state + (::modal state) + (update-in [::modal :props] merge props)))))) (defn hide [] @@ -42,12 +50,18 @@ (ptk/reify ::update-modal ptk/UpdateEvent (update [_ state] - (c/update state ::modal merge options)))) + (cond-> state + (::modal state) + (c/update ::modal merge options))))) (defn show! [type props] (st/emit! (show type props))) +(defn update-props! + [type props] + (st/emit! (update-props type props))) + (defn allow-click-outside! [] (st/emit! (update {:allow-click-outside true}))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 5fcd337cd..959f8a059 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -741,7 +741,7 @@ (watch [_ state stream] (let [selected (get-in state [:workspace-local :selected])] (rx/of (delete-shapes selected) - dws/deselect-all))))) + (dws/deselect-all)))))) ;; --- Shape Vertical Ordering @@ -1318,7 +1318,7 @@ :grow-type (if (> (count text) 100) :auto-height :auto-width) :content (as-content text)})] (rx/of dwc/start-undo-transaction - dws/deselect-all + (dws/deselect-all) (add-shape shape) (dwc/rehash-shape-frame-relationship [id]) dwc/commit-undo-transaction))))) @@ -1444,22 +1444,23 @@ (defn change-canvas-color [color] - (s/assert string? color) + ;; TODO: Create a color spec + #_(s/assert string? color) (ptk/reify ::change-canvas-color ptk/WatchEvent (watch [_ state stream] (let [page-id (get state :current-page-id) options (dwc/lookup-page-options state page-id) - ccolor (:background options)] + previus-color (:background options)] (rx/of (dwc/commit-changes [{:type :set-option :page-id page-id :option :background - :value color}] + :value (:color color)}] [{:type :set-option :page-id page-id :option :background - :value ccolor}] + :value previus-color}] {:commit-local? true})))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1530,7 +1531,7 @@ (select-for-drawing :text)) "ctrl+c" #(st/emit! copy-selected) "ctrl+v" #(st/emit! paste) - "escape" #(st/emit! :interrupt deselect-all) + "escape" #(st/emit! :interrupt (deselect-all true)) "del" #(st/emit! delete-selected) "backspace" #(st/emit! delete-selected) "ctrl+up" #(st/emit! (vertical-order-selected :up)) diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index c61420901..bfe114418 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -299,7 +299,7 @@ (rx/of dwc/start-undo-transaction) (rx/empty)) - (rx/of dw/deselect-all + (rx/of (dw/deselect-all) (dw/add-shape shape)))))))))) (def close-drawing-path diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs index 9d7019087..feacd4fbf 100644 --- a/frontend/src/app/main/data/workspace/grid.cljs +++ b/frontend/src/app/main/data/workspace/grid.cljs @@ -21,7 +21,7 @@ (defonce ^:private default-square-params {:size 16 - :color {:value "#59B9E2" + :color {:color "#59B9E2" :opacity 0.2}}) (defonce ^:private default-layout-params @@ -30,7 +30,7 @@ :item-length nil :gutter 8 :margin 0 - :color {:value "#DE4762" + :color {:color "#DE4762" :opacity 0.1}}) (defonce default-grid-params diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 60f01316c..81d1b7028 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -33,25 +33,32 @@ (declare sync-file) +(defn default-color-name [color] + (or (:color color) + (case (get-in color [:gradient :type]) + :linear (tr "workspace.gradients.linear") + :radial (tr "workspace.gradients.radial")))) + (defn add-color [color] - (us/assert ::us/string color) - (ptk/reify ::add-color - ptk/WatchEvent - (watch [_ state s] - (let [id (uuid/next) - rchg {:type :add-color - :color {:id id - :name color - :value color}} - uchg {:type :del-color - :id id}] - (rx/of #(assoc-in % [:workspace-local :color-for-rename] id) - (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (let [id (uuid/next) + color (assoc color + :id id + :name (default-color-name color))] + (us/assert ::cp/color color) + (ptk/reify ::add-color + ptk/WatchEvent + (watch [_ state s] + (let [rchg {:type :add-color + :color color} + uchg {:type :del-color + :id id}] + (rx/of #(assoc-in % [:workspace-local :color-for-rename] id) + (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))) (defn add-recent-color [color] - (us/assert ::us/string color) + (us/assert ::cp/recent-color color) (ptk/reify ::add-recent-color ptk/WatchEvent (watch [_ state s] diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 928e87da4..b2a91a917 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -22,6 +22,7 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] + [app.main.data.modal :as md] [app.main.streams :as ms] [app.main.worker :as uw])) @@ -66,7 +67,7 @@ (ms/mouse-up? %)) stream)] (rx/concat - (rx/of deselect-all) + (rx/of (deselect-all)) (->> ms/mouse-position (rx/scan (fn [data pos] (if data @@ -117,15 +118,26 @@ objects (dwc/lookup-page-objects state page-id)] (rx/of (dwc/expand-all-parents ids objects)))))) -(def deselect-all +(defn deselect-all "Clear all possible state of drawing, edition - or any similar action taken by the user." - (ptk/reify ::deselect-all - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local #(-> % - (assoc :selected (d/ordered-set)) - (dissoc :selected-frame)))))) + or any similar action taken by the user. + When `check-modal` the method will check if a modal is opened + and not deselect if it's true" + ([] (deselect-all false)) + + ([check-modal] + (ptk/reify ::deselect-all + ptk/UpdateEvent + (update [_ state] + + ;; Only deselect if there is no modal openned + (cond-> state + (or (not check-modal) + (not (::md/modal state))) + (update :workspace-local + #(-> % + (assoc :selected (d/ordered-set)) + (dissoc :selected-frame)))))))) ;; --- Select Shapes (By selrect) diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 7169782d6..7146814df 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -26,7 +26,8 @@ [app.main.ui.shapes.path :as path] [app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.text :as text] - [app.main.ui.shapes.group :as group])) + [app.main.ui.shapes.group :as group] + [app.main.ui.shapes.shape :refer [shape-container]])) (def ^:private default-color "#E8E9EA") ;; $color-canvas @@ -56,7 +57,8 @@ [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape)) shape (geom/transform-shape shape)] - [:& frame-shape {:shape shape :childs childs}])))) + [:> shape-container {:shape shape} + [:& frame-shape {:shape shape :childs childs}]])))) (defn group-wrapper-factory [objects] @@ -78,10 +80,8 @@ frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] (when (and shape (not (:hidden shape))) (let [shape (geom/transform-shape frame shape) - opts #js {:shape shape} - filter-id (filters/get-filter-id)] - [:g {:filter (filters/filter-str filter-id shape)} - [:& filters/filters {:filter-id filter-id :shape shape}] + opts #js {:shape shape}] + [:> shape-container {:shape shape} (case (:type shape) :curve [:> path/path-shape opts] :text [:> text/text-shape opts] diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs new file mode 100644 index 000000000..c4acf00ce --- /dev/null +++ b/frontend/src/app/main/ui/components/color_bullet.cljs @@ -0,0 +1,41 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.components.color-bullet + (:require + [rumext.alpha :as mf] + [app.util.i18n :as i18n :refer [tr]] + [app.util.color :as uc])) + +(mf/defc color-bullet [{:keys [color on-click]}] + (if (uc/multiple? color) + [:div.color-bullet.multiple {:on-click #(when on-click (on-click %))}] + + ;; No multiple selection + (let [color (if (string? color) {:color color :opacity 1} color)] + [:div.color-bullet {:on-click #(when on-click (on-click %))} + (when (not (:gradient color)) + [:div.color-bullet-left {:style {:background (uc/color->background (assoc color :opacity 1))}}]) + + [:div.color-bullet-right {:style {:background (uc/color->background color)}}]]))) + +(defn gradient-type->string [type] + (case type + :linear (tr "workspace.gradients.linear") + :radial (tr "workspace.gradients.radial") + (str "???" type))) + +(mf/defc color-name [{:keys [color size on-click on-double-click]}] + (let [color (if (string? color) {:color color :opacity 1} color) + {:keys [name color opacity gradient]} color + color-str (or name color (gradient-type->string (:type gradient)))] + (when (= size :big) + [:span.color-text {:on-click #(when on-click (on-click %)) + :on-double-click #(when on-double-click (on-double-click %)) + :title name } color-str]))) diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 9b56bc217..27df726cd 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -12,3 +12,5 @@ [rumext.alpha :as mf])) (def embed-ctx (mf/create-context false)) + +(def render-ctx (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index e3894e042..4b2d36256 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -20,10 +20,10 @@ (:import goog.events.EventType)) (defn- on-esc-clicked - [event] - (when (k/esc? event) - (st/emit! (dm/hide)) - (dom/stop-propagation event))) + [event allow-click-outside] + (when (and (k/esc? event) (not allow-click-outside)) + (do (dom/stop-propagation event) + (st/emit! (dm/hide))))) (defn- on-pop-state [event] @@ -43,11 +43,14 @@ (st/emit! (dm/hide))))) (defn- on-click-outside - [event wrapper-ref allow-click-outside] + [event wrapper-ref type allow-click-outside] (let [wrapper (mf/ref-val wrapper-ref) current (dom/get-target event)] - (when (and wrapper (not allow-click-outside) (not (.contains wrapper current))) + (when (and wrapper + (not allow-click-outside) + (not (.contains wrapper current)) + (not (= type (keyword (.getAttribute current "data-allow-click-modal"))))) (dom/stop-propagation event) (dom/prevent-default event) (st/emit! (dm/hide))))) @@ -59,16 +62,23 @@ (let [data (unchecked-get props "data") wrapper-ref (mf/use-ref nil) + allow-click-outside (:allow-click-outside data) + handle-click-outside (fn [event] - (on-click-outside event wrapper-ref (:allow-click-outside data)))] + (on-click-outside event wrapper-ref (:type data) allow-click-outside)) + + handle-keydown + (fn [event] + (on-esc-clicked event allow-click-outside))] (mf/use-layout-effect + (mf/deps allow-click-outside) (fn [] - (let [keys [(events/listen js/document EventType.KEYDOWN on-esc-clicked) + (let [keys [(events/listen js/document EventType.KEYDOWN handle-keydown) (events/listen js/window EventType.POPSTATE on-pop-state) (events/listen js/document EventType.CLICK handle-click-outside)]] - #(for [key keys] + #(doseq [key keys] (events/unlistenByKey key))))) [:div.modal-wrapper {:ref wrapper-ref} diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 185c5e1f4..27083b0ab 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -9,8 +9,10 @@ (ns app.main.ui.shapes.attrs (:require + [rumext.alpha :as mf] [cuerdas.core :as str] - [app.util.object :as obj])) + [app.util.object :as obj] + [app.main.ui.context :as muc])) (defn- stroke-type->dasharray [style] @@ -20,21 +22,37 @@ :dashed "10,10" nil)) +(defn add-border-radius [attrs shape] + (obj/merge! attrs #js {:rx (:rx shape) + :ry (:ry shape)})) + +(defn add-fill [attrs shape render-id] + (let [fill-color-gradient-id (str "fill-color-gradient_" render-id)] + (if (:fill-color-gradient shape) + (obj/merge! attrs #js {:fill (str/format "url(#%s)" fill-color-gradient-id)}) + (obj/merge! attrs #js {:fill (or (:fill-color shape) "transparent") + :fillOpacity (:fill-opacity shape nil)})))) + +(defn add-stroke [attrs shape render-id] + (let [stroke-style (:stroke-style shape :none) + stroke-color-gradient-id (str "stroke-color-gradient_" render-id)] + (if (not= stroke-style :none) + (if (:stroke-color-gradient shape) + (obj/merge! attrs + #js {:stroke (str/format "url(#%s)" stroke-color-gradient-id) + :strokeWidth (:stroke-width shape 1) + :strokeDasharray (stroke-type->dasharray stroke-style)}) + (obj/merge! attrs + #js {:stroke (:stroke-color shape nil) + :strokeWidth (:stroke-width shape 1) + :strokeOpacity (:stroke-opacity shape nil) + :strokeDasharray (stroke-type->dasharray stroke-style)})))) + attrs) + (defn extract-style-attrs - ([shape] (extract-style-attrs shape nil)) - ([shape gradient-id] - (let [stroke-style (:stroke-style shape :none) - attrs #js {:rx (:rx shape nil) - :ry (:ry shape nil)} - attrs (obj/merge! attrs - (if gradient-id - #js {:fill (str/format "url(#%s)" gradient-id)} - #js {:fill (or (:fill-color shape) "transparent") - :fillOpacity (:fill-opacity shape nil)}))] - (when (not= stroke-style :none) - (obj/merge! attrs - #js {:stroke (:stroke-color shape nil) - :strokeWidth (:stroke-width shape 1) - :strokeOpacity (:stroke-opacity shape nil) - :strokeDasharray (stroke-type->dasharray stroke-style)})) - attrs))) + ([shape] + (let [render-id (mf/use-ctx muc/render-ctx)] + (-> (obj/new) + (add-border-radius shape) + (add-fill shape render-id) + (add-stroke shape render-id))))) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 5f583b2dc..3fafec419 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -25,8 +25,9 @@ (str/fmt "url(#$0)" [filter-id]))) (mf/defc color-matrix - [{:keys [color opacity]}] - (let [[r g b a] (color/hex->rgba color opacity) + [{:keys [color]}] + (let [{:keys [color opacity]} color + [r g b a] (color/hex->rgba color opacity) [r g b] [(/ r 255) (/ g 255) (/ b 255)]] [:feColorMatrix {:type "matrix" @@ -36,7 +37,7 @@ [{:keys [filter-id filter shape]}] (let [{:keys [x y width height]} (:selrect shape) - {:keys [id in-filter color opacity offset-x offset-y blur spread]} filter] + {:keys [id in-filter color offset-x offset-y blur spread]} filter] [:* [:feColorMatrix {:in "SourceAlpha" :type "matrix" :values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"}] @@ -48,7 +49,7 @@ [:feOffset {:dx offset-x :dy offset-y}] [:feGaussianBlur {:stdDeviation (/ blur 2)}] - [:& color-matrix {:color color :opacity opacity}] + [:& color-matrix {:color color}] [:feBlend {:mode "normal" :in2 in-filter @@ -58,7 +59,7 @@ [{:keys [filter-id filter shape]}] (let [{:keys [x y width height]} (:selrect shape) - {:keys [id in-filter color opacity offset-x offset-y blur spread]} filter] + {:keys [id in-filter color offset-x offset-y blur spread]} filter] [:* [:feColorMatrix {:in "SourceAlpha" :type "matrix" :values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" @@ -78,7 +79,7 @@ :k2 "-1" :k3 "1"}] - [:& color-matrix {:color color :opacity opacity}] + [:& color-matrix {:color color}] [:feBlend {:mode "normal" :in2 in-filter @@ -122,45 +123,42 @@ [filter-x filter-y filter-width filter-height] (get-filters-bounds shape filters)] (when (seq filters) - [:defs - [:filter {:id filter-id - :x filter-x :y filter-y - :width filter-width :height filter-height - :filterUnits "userSpaceOnUse" - :color-interpolation-filters "sRGB"} + [:filter {:id filter-id + :x filter-x :y filter-y + :width filter-width :height filter-height + :filterUnits "userSpaceOnUse" + :color-interpolation-filters "sRGB"} - (let [;; Add as a paramter the input filter - drop-shadow-filters (->> filters (filter #(= :drop-shadow (:style %)))) - drop-shadow-filters (->> drop-shadow-filters - (map #(str "filter" (:id %))) - (cons "BackgroundImageFix") - (map add-in-filter drop-shadow-filters)) + (let [;; Add as a paramter the input filter + drop-shadow-filters (->> filters (filter #(= :drop-shadow (:style %)))) + drop-shadow-filters (->> drop-shadow-filters + (map #(str "filter" (:id %))) + (cons "BackgroundImageFix") + (map add-in-filter drop-shadow-filters)) - inner-shadow-filters (->> filters (filter #(= :inner-shadow (:style %)))) - inner-shadow-filters (->> inner-shadow-filters + inner-shadow-filters (->> filters (filter #(= :inner-shadow (:style %)))) + inner-shadow-filters (->> inner-shadow-filters (map #(str "filter" (:id %))) (cons "shape") (map add-in-filter inner-shadow-filters))] - [:* - [:feFlood {:flood-opacity 0 :result "BackgroundImageFix"}] - (for [{:keys [id type] :as filter} drop-shadow-filters] - [:& drop-shadow-filter {:key id + [:* + [:feFlood {:flood-opacity 0 :result "BackgroundImageFix"}] + (for [{:keys [id type] :as filter} drop-shadow-filters] + [:& drop-shadow-filter {:key id + :filter-id filter-id + :filter filter + :shape shape}]) + + [:feBlend {:mode "normal" + :in "SourceGraphic" + :in2 (if (seq drop-shadow-filters) + (str "filter" (:id (last drop-shadow-filters))) + "BackgroundImageFix") + :result "shape"}] + + (for [{:keys [id type] :as filter} inner-shadow-filters] + [:& inner-shadow-filter {:key id :filter-id filter-id :filter filter - :shape shape}]) - - [:feBlend {:mode "normal" - :in "SourceGraphic" - :in2 (if (seq drop-shadow-filters) - (str "filter" (:id (last drop-shadow-filters))) - "BackgroundImageFix") - :result "shape"}] - - (for [{:keys [id type] :as filter} inner-shadow-filters] - [:& inner-shadow-filter {:key id - :filter-id filter-id - :filter filter - :shape shape}]) - ]) - ]]))) + :shape shape}])])]))) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index f86b025b0..120e623dd 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -11,11 +11,12 @@ (:require [rumext.alpha :as mf] [cuerdas.core :as str] - [goog.object :as gobj] + [app.util.object :as obj] [app.common.uuid :as uuid] + [app.main.ui.context :as muc] [app.common.geom.point :as gpt])) -(mf/defc linear-gradient [{:keys [id shape gradient]}] +(mf/defc linear-gradient [{:keys [id gradient shape]}] (let [{:keys [x y width height]} shape] [:defs [:linearGradient {:id id @@ -29,12 +30,12 @@ :stop-color color :stop-opacity opacity}])]])) -(mf/defc radial-gradient [{:keys [id shape gradient]}] +(mf/defc radial-gradient [{:keys [id gradient shape]}] (let [{:keys [x y width height]} shape] [:defs - (let [translate-vec (gpt/point (+ x (* width (:start-x gradient))) + (let [[x y] (if (= (:type shape) :frame) [0 0] [x y]) + translate-vec (gpt/point (+ x (* width (:start-x gradient))) (+ y (* height (:start-y gradient)))) - gradient-vec (gpt/to-vec (gpt/point (* width (:start-x gradient)) (* height (:start-y gradient))) @@ -50,8 +51,8 @@ scale-factor-x (* scale-factor-y (:width gradient)) scale-vec (gpt/point (* scale-factor-y (/ height 2)) - (* scale-factor-x (/ width 2)) - ) + (* scale-factor-x (/ width 2))) + tr-translate (str/fmt "translate(%s, %s)" (:x translate-vec) (:y translate-vec)) tr-rotate (str/fmt "rotate(%s)" angle) tr-scale (str/fmt "scale(%s, %s)" (:x scale-vec) (:y scale-vec)) @@ -71,8 +72,15 @@ (mf/defc gradient {::mf/wrap-props false} [props] - (let [gradient (gobj/get props "gradient")] - (case (:type gradient) - :linear [:> linear-gradient props] - :radial [:> radial-gradient props] - nil))) + (let [attr (obj/get props "attr") + shape (obj/get props "shape") + render-id (mf/use-ctx muc/render-ctx) + id (str (name attr) "_" render-id) + gradient (get shape attr) + gradient-props #js {:id id + :gradient gradient + :shape shape}] + (when gradient + (case (:type gradient) + :linear [:> linear-gradient gradient-props] + :radial [:> radial-gradient gradient-props])))) diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs index 5cac4b6b3..f2c8e98e9 100644 --- a/frontend/src/app/main/ui/shapes/rect.cljs +++ b/frontend/src/app/main/ui/shapes/rect.cljs @@ -27,9 +27,7 @@ {:keys [id x y width height]} shape transform (geom/transform-matrix shape) - gradient-id (when (:fill-color-gradient shape) (str (uuid/next))) - - props (-> (attrs/extract-style-attrs shape gradient-id) + props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:x x :y y @@ -38,12 +36,6 @@ :width width :height height}))] - - [:* - (when gradient-id - [:& gradient {:id gradient-id - :shape shape - :gradient (:fill-color-gradient shape)}]) - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name "rect"}]])) + [:& shape-custom-stroke {:shape shape + :base-props props + :elem-name "rect"}])) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index a4202b844..0567c1074 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -7,5 +7,33 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.shapes.shape) +(ns app.main.ui.shapes.shape + (:require + [rumext.alpha :as mf] + [app.util.object :as obj] + [app.common.uuid :as uuid] + [app.main.ui.shapes.filters :as filters] + [app.main.ui.shapes.gradients :as grad] + [app.main.ui.context :as muc])) +(mf/defc shape-container + {::mf/wrap-props false} + [props] + + (let [shape (unchecked-get props "shape") + children (unchecked-get props "children") + render-id (mf/use-memo #(str (uuid/next))) + filter-id (str "filter_" render-id) + group-props (-> props + (obj/clone) + (obj/without ["shape" "children"]) + (obj/set! "className" "shape") + (obj/set! "filter" (filters/filter-str filter-id shape)))] + [:& (mf/provider muc/render-ctx) {:value render-id} + [:> :g group-props + [:defs + [:& filters/filters {:shape shape :filter-id filter-id}] + [:& grad/gradient {:shape shape :attr :fill-color-gradient}] + [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]] + + children]])) diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index 05707f50b..ba773fc44 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -68,17 +68,25 @@ fill-color (obj/get data "fill-color" fill) fill-opacity (obj/get data "fill-opacity" opacity) + fill-color-gradient (obj/get data "fill-color-gradient" nil) + fill-color-gradient (when fill-color-gradient + (-> (js->clj fill-color-gradient :keywordize-keys true) + (update :type keyword))) + fill-color-ref-id (obj/get data "fill-color-ref-id") fill-color-ref-file (obj/get data "fill-color-ref-file") [r g b a] (uc/hex->rgba fill-color fill-opacity) + background (if fill-color-gradient + (uc/gradient->css (js->clj fill-color-gradient)) + (str/format "rgba(%s, %s, %s, %s)" r g b a)) fontsdb (deref fonts/fontsdb) base #js {:textDecoration text-decoration - :color (str/format "rgba(%s, %s, %s, %s)" r g b a) :textTransform text-transform - :lineHeight (or line-height "inherit")}] + :lineHeight (or line-height "inherit") + "--text-color" background}] (when (and (string? letter-spacing) (pos? (alength letter-spacing))) @@ -167,7 +175,7 @@ (if (string? text) (let [style (generate-text-styles (clj->js node))] - [:span {:style style :key index} (if (= text "") "\u00A0" text)]) + [:span.text-node {:style style} (if (= text "") "\u00A0" text)]) (let [children (map-indexed (fn [index node] (mf/element text-node {:index index :node node :key index})) children)] @@ -179,13 +187,15 @@ {:key index :style style :xmlns "http://www.w3.org/1999/xhtml"} - (when (not (nil? @embeded-fonts)) - [:style @embeded-fonts]) + [:* + [:style ".text-node { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] + (when (not (nil? @embeded-fonts)) + [:style @embeded-fonts])] children]) "paragraph-set" (let [style #js {:display "inline-block"}] - [:div.paragraphs {:key index :style style} children]) + [:div.paragraphs {:key index :style style} children]) "paragraph" (let [style (generate-paragraph-styles (clj->js node))] diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 2ed021856..7ceb6166c 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -29,7 +29,8 @@ [app.util.object :as obj] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom])) + [app.common.geom.shapes :as geom] + [app.main.ui.shapes.shape :refer [shape-container]])) (defn on-mouse-down [event {:keys [interactions] :as shape}] @@ -54,14 +55,11 @@ on-mouse-down (mf/use-callback (mf/deps shape) - #(on-mouse-down % shape)) + #(on-mouse-down % shape))] - filter-id (filters/get-filter-id)] - - [:g.shape {:on-mouse-down on-mouse-down - :cursor (when (:interactions shape) "pointer") - :filter (filters/filter-str filter-id shape)} - [:& filters/filters {:filter-id filter-id :shape shape}] + [:> shape-container {:shape shape + :on-mouse-down on-mouse-down + :cursor (when (:interactions shape) "pointer")} [:& component {:shape shape :frame frame :childs childs diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index bfda8cd1b..9aa0b0166 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -19,6 +19,7 @@ [app.main.data.workspace :as udw] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.color-bullet :as cb] [app.main.ui.icons :as i] [app.main.ui.keyboard :as kbd] [app.util.color :refer [hex->rgb]] @@ -55,14 +56,13 @@ (fn [event] (let [ids (get-in @st/state [:workspace-local :selected])] (if (kbd/shift? event) - (st/emit! (mdc/change-stroke ids (:value color) id file-id)) - (st/emit! (mdc/change-fill ids (:value color) id file-id)))))] + (st/emit! (mdc/change-stroke ids color)) + (st/emit! (mdc/change-fill ids color)))))] [:div.color-cell {:class (str "cell-"(name size)) - :key (or (str (:id color)) (:value color)) :on-click select-color} - [:span.color {:style {:background (:value color)}}] - (when (= size :big) [:span.color-text {:title (:name color) } (or (:name color) (:value color))])])) + [:& cb/color-bullet {:color color}] + [:& cb/color-name {:color color :size size}]])) (mf/defc palette [{:keys [left-sidebar? current-colors recent-colors file-colors shared-libs selected size]}] @@ -138,9 +138,9 @@ (when (= selected (:id cur-library)) i/tick) [:div.library-name (str (:name cur-library) " " (str/format "(%s)" (count colors)))] [:div.color-sample - (for [[idx {:keys [id value]}] (map-indexed vector (take 7 colors))] - [:div.color-bullet {:key (str "color-" idx) - :style {:background-color value}}])]])) + (for [[idx {:keys [id color]}] (map-indexed vector (take 7 colors))] + [:& cb/color-bullet {:key (str "color-" idx) + :color color}])]])) [:li.palette-library @@ -149,9 +149,9 @@ [:div.library-name (str (t locale "workspace.libraries.colors.file-library") (str/format " (%s)" (count file-colors)))] [:div.color-sample - (for [[idx {:keys [value]}] (map-indexed vector (take 7 (vals file-colors))) ] - [:div.color-bullet {:key (str "color-" idx) - :style {:background-color value}}])]] + (for [[idx color] (map-indexed vector (take 7 (vals file-colors))) ] + [:& cb/color-bullet {:key (str "color-" idx) + :color color}])]] [:li.palette-library {:on-click #(st/emit! (mdc/change-palette-selected :recent))} @@ -159,9 +159,9 @@ [:div.library-name (str (t locale "workspace.libraries.colors.recent-colors") (str/format " (%s)" (count recent-colors)))] [:div.color-sample - (for [[idx value] (map-indexed vector (take 7 (reverse recent-colors))) ] - [:div.color-bullet {:key (str "color-" idx) - :style {:background-color value}}])]] + (for [[idx color] (map-indexed vector (take 7 (reverse recent-colors))) ] + [:& cb/color-bullet {:key (str "color-" idx) + :color color}])]] [:hr.dropdown-separator] @@ -191,14 +191,8 @@ [:span.right-arrow {:on-click on-right-arrow-click} i/arrow-slide]])) -(defn recent->colors [recent-colors] - (map #(hash-map :value %) (reverse (or recent-colors [])))) - -(defn file->colors [file-colors] - (map #(select-keys % [:id :value :name]) (vals file-colors))) - (defn library->colors [shared-libs selected] - (map #(merge {:file-id selected} (select-keys % [:id :value :name])) + (map #(merge {:file-id selected} %) (vals (get-in shared-libs [selected :data :colors])))) (mf/defc colorpalette @@ -217,21 +211,21 @@ (reset! current-library-colors (into [] (cond - (= selected :recent) (recent->colors recent-colors) - (= selected :file) (file->colors file-colors) + (= selected :recent) (reverse recent-colors) + (= selected :file) (vals file-colors) :else (library->colors shared-libs selected)))))) (mf/use-effect (mf/deps recent-colors) (fn [] (when (= selected :recent) - (reset! current-library-colors (into [] (recent->colors recent-colors)))))) + (reset! current-library-colors (reverse recent-colors))))) (mf/use-effect (mf/deps file-colors) (fn [] (when (= selected :file) - (reset! current-library-colors (into [] (file->colors file-colors)))))) + (reset! current-library-colors (into [] (vals file-colors)))))) [:& palette {:left-sidebar? left-sidebar? :current-colors @current-library-colors diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index a8901ba2e..dc81151e4 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -21,10 +21,16 @@ [app.main.store :as st] [app.main.refs :as refs] [app.main.data.workspace.libraries :as dwl] - [app.main.data.colors :as dwc] + [app.main.data.colors :as dc] [app.main.data.modal :as modal] [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]])) + [app.util.i18n :as i18n :refer [t]] + [app.main.ui.workspace.colorpicker.gradients :refer [gradients]] + [app.main.ui.workspace.colorpicker.harmony :refer [harmony-selector]] + [app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]] + [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]] + [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]] + [app.main.ui.workspace.colorpicker.libraries :refer [libraries]])) ;; --- Refs @@ -43,414 +49,15 @@ (def viewport (l/derived (l/in [:workspace-local :vport]) st/state)) +(def editing-spot-state-ref + (l/derived (l/in [:workspace-local :editing-stop]) st/state)) + +(def current-gradient-ref + (l/derived (l/in [:workspace-local :current-gradient]) st/state)) ;; --- Color Picker Modal -(mf/defc value-saturation-selector [{:keys [hue saturation value on-change]}] - (let [dragging? (mf/use-state false) - calculate-pos - (fn [ev] - (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect) - {:keys [x y]} (-> ev dom/get-client-position) - px (math/clamp (/ (- x left) (- right left)) 0 1) - py (* 255 (- 1 (math/clamp (/ (- y top) (- bottom top)) 0 1)))] - (on-change px py)))] - [:div.value-saturation-selector - {:on-mouse-down #(reset! dragging? true) - :on-mouse-up #(reset! dragging? false) - :on-pointer-down (partial dom/capture-pointer) - :on-pointer-up (partial dom/release-pointer) - :on-click calculate-pos - :on-mouse-move #(when @dragging? (calculate-pos %))} - [:div.handler {:style {:pointer-events "none" - :left (str (* 100 saturation) "%") - :top (str (* 100 (- 1 (/ value 255))) "%")}}]])) - - -(mf/defc slider-selector [{:keys [value class min-value max-value vertical? reverse? on-change]}] - (let [min-value (or min-value 0) - max-value (or max-value 1) - dragging? (mf/use-state false) - calculate-pos - (fn [ev] - (when on-change - (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect) - {:keys [x y]} (-> ev dom/get-client-position) - unit-value (if vertical? - (math/clamp (/ (- bottom y) (- bottom top)) 0 1) - (math/clamp (/ (- x left) (- right left)) 0 1)) - unit-value (if reverse? - (math/abs (- unit-value 1.0)) - unit-value) - value (+ min-value (* unit-value (- max-value min-value)))] - (on-change value))))] - - [:div.slider-selector - {:class (str (if vertical? "vertical " "") class) - :on-mouse-down #(reset! dragging? true) - :on-mouse-up #(reset! dragging? false) - :on-pointer-down (partial dom/capture-pointer) - :on-pointer-up (partial dom/release-pointer) - :on-click calculate-pos - :on-mouse-move #(when @dragging? (calculate-pos %))} - - (let [value-percent (* (/ (- value min-value) - (- max-value min-value)) 100) - - value-percent (if reverse? - (math/abs (- value-percent 100)) - value-percent) - value-percent-str (str value-percent "%") - - style-common #js {:pointerEvents "none"} - style-horizontal (obj/merge! #js {:left value-percent-str} style-common) - style-vertical (obj/merge! #js {:bottom value-percent-str} style-common)] - [:div.handler {:style (if vertical? style-vertical style-horizontal)}])])) - - -(defn create-color-wheel - [canvas-node] - (let [ctx (.getContext canvas-node "2d") - width (obj/get canvas-node "width") - height (obj/get canvas-node "height") - radius (/ width 2) - cx (/ width 2) - cy (/ width 2) - step 0.2] - - (.clearRect ctx 0 0 width height) - - (doseq [degrees (range 0 360 step)] - (let [degrees-rad (math/radians degrees) - x (* radius (math/cos (- degrees-rad))) - y (* radius (math/sin (- degrees-rad)))] - (obj/set! ctx "strokeStyle" (str/format "hsl(%s, 100%, 50%)" degrees)) - (.beginPath ctx) - (.moveTo ctx cx cy) - (.lineTo ctx (+ cx x) (+ cy y)) - (.stroke ctx))) - - (let [grd (.createRadialGradient ctx cx cy 0 cx cx radius)] - (.addColorStop grd 0 "white") - (.addColorStop grd 1 "rgba(255, 255, 255, 0") - (obj/set! ctx "fillStyle" grd) - - (.beginPath ctx) - (.arc ctx cx cy radius 0 (* 2 math/PI) true) - (.closePath ctx) - (.fill ctx)))) - -(mf/defc ramp-selector [{:keys [color on-change]}] - (let [{hue :h saturation :s value :v alpha :alpha} color - - on-change-value-saturation - (fn [new-saturation new-value] - (let [hex (uc/hsv->hex [hue new-saturation new-value]) - [r g b] (uc/hex->rgb hex)] - (on-change {:hex hex - :r r :g g :b b - :s new-saturation - :v new-value}))) - - on-change-hue - (fn [new-hue] - (let [hex (uc/hsv->hex [new-hue saturation value]) - [r g b] (uc/hex->rgb hex)] - (on-change {:hex hex - :r r :g g :b b - :h new-hue} ))) - - on-change-opacity - (fn [new-opacity] - (on-change {:alpha new-opacity} ))] - [:* - [:& value-saturation-selector - {:hue hue - :saturation saturation - :value value - :on-change on-change-value-saturation}] - - [:div.shade-selector - [:div.color-bullet] - [:& slider-selector {:class "hue" - :max-value 360 - :value hue - :on-change on-change-hue}] - - [:& slider-selector {:class "opacity" - :max-value 1 - :value alpha - :on-change on-change-opacity}]]])) - -(defn color->point - [canvas-side hue saturation] - (let [hue-rad (math/radians (- hue)) - comp-x (* saturation (math/cos hue-rad)) - comp-y (* saturation (math/sin hue-rad)) - x (+ (/ canvas-side 2) (* comp-x (/ canvas-side 2))) - y (+ (/ canvas-side 2) (* comp-y (/ canvas-side 2)))] - (gpt/point x y))) - -(mf/defc harmony-selector [{:keys [color on-change]}] - (let [canvas-ref (mf/use-ref nil) - {hue :h saturation :s value :v alpha :alpha} color - - canvas-side 152 - pos-current (color->point canvas-side hue saturation) - pos-complement (color->point canvas-side (mod (+ hue 180) 360) saturation) - dragging? (mf/use-state false) - - calculate-pos (fn [ev] - (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect) - {:keys [x y]} (-> ev dom/get-client-position) - px (math/clamp (/ (- x left) (- right left)) 0 1) - py (math/clamp (/ (- y top) (- bottom top)) 0 1) - - px (- (* 2 px) 1) - py (- (* 2 py) 1) - - angle (math/degrees (math/atan2 px py)) - new-hue (math/precision (mod (- angle 90 ) 360) 2) - new-saturation (math/clamp (math/distance [px py] [0 0]) 0 1) - hex (uc/hsv->hex [new-hue new-saturation value]) - [r g b] (uc/hex->rgb hex)] - (on-change {:hex hex - :r r :g g :b b - :h new-hue - :s new-saturation}))) - - on-change-value (fn [new-value] - (let [hex (uc/hsv->hex [hue saturation new-value]) - [r g b] (uc/hex->rgb hex)] - (on-change {:hex hex - :r r :g g :b b - :v new-value}))) - on-complement-click (fn [ev] - (let [new-hue (mod (+ hue 180) 360) - hex (uc/hsv->hex [new-hue saturation value]) - [r g b] (uc/hex->rgb hex)] - (on-change {:hex hex - :r r :g g :b b - :h new-hue - :s saturation}))) - - on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))] - - (mf/use-effect - (mf/deps canvas-ref) - (fn [] (when canvas-ref - (create-color-wheel (mf/ref-val canvas-ref))))) - - [:div.harmony-selector - [:div.hue-wheel-wrapper - [:canvas.hue-wheel - {:ref canvas-ref - :width canvas-side - :height canvas-side - :on-mouse-down #(reset! dragging? true) - :on-mouse-up #(reset! dragging? false) - :on-pointer-down (partial dom/capture-pointer) - :on-pointer-up (partial dom/release-pointer) - :on-click calculate-pos - :on-mouse-move #(when @dragging? (calculate-pos %))}] - [:div.handler {:style {:pointer-events "none" - :left (:x pos-current) - :top (:y pos-current)}}] - [:div.handler.complement {:style {:left (:x pos-complement) - :top (:y pos-complement) - :cursor "pointer"} - :on-click on-complement-click}]] - [:div.handlers-wrapper - [:& slider-selector {:class "value" - :vertical? true - :reverse? true - :value value - :max-value 255 - :vertical true - :on-change on-change-value}] - [:& slider-selector {:class "opacity" - :vertical? true - :value alpha - :max-value 1 - :vertical true - :on-change on-change-opacity}]]])) - -(mf/defc hsva-selector [{:keys [color on-change]}] - (let [{hue :h saturation :s value :v alpha :alpha} color - handle-change-slider (fn [key] - (fn [new-value] - (let [change (hash-map key new-value) - {:keys [h s v]} (merge color change) - hex (uc/hsv->hex [h s v]) - [r g b] (uc/hex->rgb hex)] - (on-change (merge change - {:hex hex - :r r :g g :b b}))))) - on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))] - [:div.hsva-selector - [:span.hsva-selector-label "H"] - [:& slider-selector - {:class "hue" :max-value 360 :value hue :on-change (handle-change-slider :h)}] - - [:span.hsva-selector-label "S"] - [:& slider-selector - {:class "saturation" :max-value 1 :value saturation :on-change (handle-change-slider :s)}] - - [:span.hsva-selector-label "V"] - [:& slider-selector - {:class "value" :reverse? true :max-value 255 :value value :on-change (handle-change-slider :v)}] - - [:span.hsva-selector-label "A"] - [:& slider-selector - {:class "opacity" :max-value 1 :value alpha :on-change on-change-opacity}]])) - -(mf/defc color-inputs [{:keys [type color on-change]}] - (let [{red :r green :g blue :b - hue :h saturation :s value :v - hex :hex alpha :alpha} color - - parse-hex (fn [val] (if (= (first val) \#) val (str \# val))) - - refs {:hex (mf/use-ref nil) - :r (mf/use-ref nil) - :g (mf/use-ref nil) - :b (mf/use-ref nil) - :h (mf/use-ref nil) - :s (mf/use-ref nil) - :v (mf/use-ref nil) - :alpha (mf/use-ref nil)} - - on-change-hex - (fn [e] - (let [val (-> e dom/get-target-val parse-hex)] - (when (uc/hex? val) - (let [[r g b] (uc/hex->rgb val) - [h s v] (uc/hex->hsv hex)] - (on-change {:hex val - :h h :s s :v v - :r r :g g :b b}))))) - - on-change-property - (fn [property max-value] - (fn [e] - (let [val (-> e dom/get-target-val (math/clamp 0 max-value)) - val (if (#{:s} property) (/ val 100) val)] - (when (not (nil? val)) - (if (#{:r :g :b} property) - (let [{:keys [r g b]} (merge color (hash-map property val)) - hex (uc/rgb->hex [r g b]) - [h s v] (uc/hex->hsv hex)] - (on-change {:hex hex - :h h :s s :v v - :r r :g g :b b})) - - (let [{:keys [h s v]} (merge color (hash-map property val)) - hex (uc/hsv->hex [h s v]) - [r g b] (uc/hex->rgb hex)] - (on-change {:hex hex - :h h :s s :v v - :r r :g g :b b}))))))) - - on-change-opacity - (fn [e] - (when-let [new-alpha (-> e dom/get-target-val (math/clamp 0 100) (/ 100))] - (on-change {:alpha new-alpha})))] - - - ;; Updates the inputs values when a property is changed in the parent - (mf/use-effect - (mf/deps color type) - (fn [] - (doseq [ref-key (keys refs)] - (let [property-val (get color ref-key) - property-ref (get refs ref-key)] - (when (and property-val property-ref) - (when-let [node (mf/ref-val property-ref)] - (case ref-key - (:s :alpha) (dom/set-value! node (math/round (* property-val 100))) - :hex (dom/set-value! node property-val) - (dom/set-value! node (math/round property-val))))))))) - - [:div.color-values - [:input {:id "hex-value" - :ref (:hex refs) - :default-value hex - :on-change on-change-hex}] - - (if (= type :rgb) - [:* - [:input {:id "red-value" - :ref (:r refs) - :type "number" - :min 0 - :max 255 - :default-value red - :on-change (on-change-property :r 255)}] - - [:input {:id "green-value" - :ref (:g refs) - :type "number" - :min 0 - :max 255 - :default-value green - :on-change (on-change-property :g 255)}] - - [:input {:id "blue-value" - :ref (:b refs) - :type "number" - :min 0 - :max 255 - :default-value blue - :on-change (on-change-property :b 255)}]] - [:* - [:input {:id "hue-value" - :ref (:h refs) - :type "number" - :min 0 - :max 360 - :default-value hue - :on-change (on-change-property :h 360)}] - - [:input {:id "saturation-value" - :ref (:s refs) - :type "number" - :min 0 - :max 100 - :step 1 - :default-value saturation - :on-change (on-change-property :s 100)}] - - [:input {:id "value-value" - :ref (:v refs) - :type "number" - :min 0 - :max 255 - :default-value value - :on-change (on-change-property :v 255)}]]) - - [:input.alpha-value {:id "alpha-value" - :ref (:alpha refs) - :type "number" - :min 0 - :step 1 - :max 100 - :default-value (if (= alpha :multiple) "" (math/precision alpha 2)) - :on-change on-change-opacity}] - - [:label.hex-label {:for "hex-value"} "HEX"] - (if (= type :rgb) - [:* - [:label.red-label {:for "red-value"} "R"] - [:label.green-label {:for "green-value"} "G"] - [:label.blue-label {:for "blue-value"} "B"]] - [:* - [:label.red-label {:for "hue-value"} "H"] - [:label.green-label {:for "saturation-value"} "S"] - [:label.blue-label {:for "value-value"} "V"]]) - [:label.alpha-label {:for "alpha-value"} "A"]])) - - -(defn as-color-components [value opacity] +(defn color->components [value opacity] (let [value (if (uc/hex? value) value "#000000") [r g b] (uc/hex->rgb value) [h s v] (uc/hex->hsv value)] @@ -460,58 +67,139 @@ :r r :g g :b b :h h :s s :v v})) -(mf/defc colorpicker - [{:keys [value opacity on-change on-accept]}] - (let [current-color (mf/use-state (as-color-components value opacity)) +(defn data->state [{:keys [color opacity gradient]}] + (let [type (cond + (nil? gradient) :color + (= :linear (:type gradient)) :linear-gradient + (= :radial (:type gradient)) :radial-gradient) + parse-stop (fn [{:keys [offset color opacity]}] + (vector offset (color->components color opacity))) + + stops (when gradient + (map parse-stop (:stops gradient))) + + current-color (if (nil? gradient) + (color->components color opacity) + (-> stops first second)) + + gradient-data (select-keys gradient [:start-x :start-y + :end-x :end-y + :width])] + + (cond-> {:type type + :current-color current-color} + gradient (assoc :gradient-data gradient-data) + stops (assoc :stops (into {} stops)) + stops (assoc :editing-stop (-> stops first first))))) + +(defn state->data [{:keys [type current-color stops gradient-data]}] + (if (= type :color) + {:color (:hex current-color) + :opacity (:alpha current-color)} + + (let [gradient-type (case type + :linear-gradient :linear + :radial-gradient :radial) + parse-stop (fn [[offset {:keys [hex alpha]}]] + (hash-map :offset offset + :color hex + :opacity alpha))] + {:gradient (-> {:type gradient-type + :stops (mapv parse-stop stops)} + (merge gradient-data))}))) + +(defn create-gradient-data [type] + {:start-x 0.5 + :start-y (if (= type :linear-gradient) 0.0 0.5) + :end-x 0.5 + :end-y 1 + :width 1.0}) + +(mf/defc colorpicker + [{:keys [data disable-gradient disable-opacity on-change on-accept]}] + (let [state (mf/use-state (data->state data)) active-tab (mf/use-state :ramp #_:harmony #_:hsva) - selected-library (mf/use-state "recent") - current-library-colors (mf/use-state []) + locale (mf/deref i18n/locale) + ref-picker (mf/use-ref) - file-colors (mf/deref refs/workspace-file-colors) - shared-libs (mf/deref refs/workspace-libraries) - recent-colors (mf/deref refs/workspace-recent-colors) + dirty? (mf/use-var false) + last-color (mf/use-var data) picking-color? (mf/deref picking-color?) picked-color (mf/deref picked-color) picked-color-select (mf/deref picked-color-select) picked-shift? (mf/deref picked-shift?) - locale (mf/deref i18n/locale) + editing-spot-state (mf/deref editing-spot-state-ref) + current-gradient (mf/deref current-gradient-ref) - value-ref (mf/use-var value) + current-color (:current-color @state) - on-change (or on-change identity) + change-tab + (fn [tab] + #(reset! active-tab tab)) - parse-selected (fn [selected] - (if (#{"recent" "file"} selected) - (keyword selected) - (uuid selected)) ) + handle-change-color + (fn [changes] + (let [editing-stop (:editing-stop @state)] + (swap! state #(cond-> % + true (update :current-color merge changes) + editing-stop (update-in [:stops editing-stop] merge changes))) + (reset! dirty? true))) - change-tab (fn [tab] #(reset! active-tab tab)) + handle-click-picker (fn [] + (if picking-color? + (do (modal/disallow-click-outside!) + (st/emit! (dc/stop-picker))) + (do (modal/allow-click-outside!) + (st/emit! (dc/start-picker))))) - handle-change-color (fn [changes] - (swap! current-color merge changes) - (when (:hex changes) - (reset! value-ref (:hex changes))) - (on-change (:hex changes (:hex @current-color)) - (:alpha changes (:alpha @current-color))))] + handle-change-stop + (fn [offset] + (when-let [offset-color (get-in @state [:stops offset])] + (do (swap! state assoc + :current-color offset-color + :editing-stop offset) - ;; Update state when there is a change in the props upstream - (mf/use-effect - (mf/deps value opacity) - (fn [] - (reset! current-color (as-color-components value opacity)))) + (st/emit! (dc/select-gradient-stop offset))))) + + on-select-library-color + (fn [color] + (reset! dirty? true) + (reset! state (data->state color))) + + on-add-library-color + (fn [color] (st/emit! (dwl/add-color (state->data @state)))) + + on-activate-gradient + (fn [type] + (fn [] + (reset! dirty? true) + (if (= type (:type @state)) + (do + (swap! state assoc :type :color) + (swap! state dissoc :editing-stop :stops :gradient-data) + (st/emit! (dc/stop-gradient))) + (let [gradient-data (create-gradient-data type)] + (swap! state assoc :type type :gradient-data gradient-data) + (when (not (:stops @state)) + (swap! state assoc + :editing-stop 0 + :stops {0 (:current-color @state) + 1 (-> (:current-color @state) + (assoc :alpha 0))}))))))] ;; Updates the CSS color variable when there is a change in the color (mf/use-effect - (mf/deps @current-color) + (mf/deps current-color) (fn [] (let [node (mf/ref-val ref-picker) - rgb [(:r @current-color) (:g @current-color) (:b @current-color)] - hue-rgb (uc/hsv->rgb [(:h @current-color) 1.0 255]) - hsl-from (uc/hsv->hsl [(:h @current-color) 0 (:v @current-color)]) - hsl-to (uc/hsv->hsl [(:h @current-color) 1 (:v @current-color)]) + {:keys [r g b h s v]} current-color + rgb [r g b] + hue-rgb (uc/hsv->rgb [h 1.0 255]) + hsl-from (uc/hsv->hsl [h 0.0 v]) + hsl-to (uc/hsv->hsl [h 1.0 v]) format-hsl (fn [[h s l]] (str/fmt "hsl(%s, %s, %s)" @@ -523,137 +211,131 @@ (dom/set-css-property node "--saturation-grad-from" (format-hsl hsl-from)) (dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to))))) - ;; Load library colors when the select is changed - (mf/use-effect - (mf/deps @selected-library) - (fn [] - (let [mapped-colors - (cond - (= @selected-library "recent") - (map #(hash-map :value %) (reverse (or recent-colors []))) - - (= @selected-library "file") - (map #(select-keys % [:id :value]) (vals file-colors)) - - :else ;; Library UUID - (map #(merge {:file-id (uuid @selected-library)} (select-keys % [:id :value])) - (vals (get-in shared-libs [(uuid @selected-library) :data :colors]))))] - (reset! current-library-colors (into [] mapped-colors))))) - - ;; If the file colors change and the file option is selected updates the state - (mf/use-effect - (mf/deps file-colors) - (fn [] (when (= @selected-library "file") - (let [colors (map #(select-keys % [:id :value]) (vals file-colors))] - (reset! current-library-colors (into [] colors)))))) - ;; When closing the modal we update the recent-color list (mf/use-effect - (fn [] (fn [] - (st/emit! (dwc/stop-picker)) - (when @value-ref - (st/emit! (dwl/add-recent-color @value-ref)))))) - - (mf/use-effect - (mf/deps picking-color? picked-color) - (fn [] (when picking-color? - (let [[r g b] (or picked-color [0 0 0]) - hex (uc/rgb->hex [r g b]) - [h s v] (uc/hex->hsv hex)] - (swap! current-color assoc - :r r :g g :b b - :h h :s s :v v - :hex hex) - (reset! value-ref hex) - (when picked-color-select - (on-change hex (:alpha @current-color) nil nil picked-shift?)))))) + #(fn [] + (st/emit! (dc/stop-picker)) + (when @last-color + (st/emit! (dwl/add-recent-color @last-color))))) + ;; Updates color when used el pixel picker (mf/use-effect (mf/deps picking-color? picked-color-select) - (fn [] (when (and picking-color? picked-color-select) - (on-change (:hex @current-color) (:alpha @current-color) nil nil picked-shift?)))) + (fn [] + (when (and picking-color? picked-color-select) + (let [[r g b alpha] picked-color + hex (uc/rgb->hex [r g b]) + [h s v] (uc/hex->hsv hex)] + (handle-change-color {:hex hex + :r r :g g :b b + :h h :s s :v v + :alpha (/ alpha 255)}))))) + + ;; Changes when another gradient handler is selected + (mf/use-effect + (mf/deps editing-spot-state) + #(when (not= editing-spot-state (:editing-stop @state)) + (handle-change-stop (or editing-spot-state 0)))) + + ;; Changes on the viewport when moving a gradient handler + (mf/use-effect + (mf/deps current-gradient) + (fn [] + (when current-gradient + (let [gradient-data (select-keys current-gradient [:start-x :start-y + :end-x :end-y + :width])] + (when (not= (:gradient-data @state) gradient-data) + (do + (reset! dirty? true) + (swap! state assoc :gradient-data gradient-data))))))) + + ;; Check if we've opened a color with gradient + (mf/use-effect + (fn [] + (when (:gradient data) + (st/emit! (dc/start-gradient (:gradient data)))) + + ;; on-unmount we stop the handlers + #(st/emit! (dc/stop-gradient)))) + + ;; Send the properties to the store + (mf/use-effect + (mf/deps @state) + (fn [] + (if @dirty? + (let [color (state->data @state)] + (reset! dirty? false) + (reset! last-color color) + (when (:gradient color) + (st/emit! (dc/start-gradient (:gradient color)))) + (on-change color))))) [:div.colorpicker {:ref ref-picker} [:div.colorpicker-content [:div.top-actions [:button.picker-btn {:class (when picking-color? "active") - :on-click (fn [] - (modal/allow-click-outside!) - (st/emit! (dwc/start-picker)))} + :on-click handle-click-picker} i/picker] - [:div.gradients-buttons - [:button.gradient.linear-gradient #_{:class "active"}] - [:button.gradient.radial-gradient]]] + (when (not disable-gradient) + [:div.gradients-buttons + [:button.gradient.linear-gradient + {:on-click (on-activate-gradient :linear-gradient) + :class (when (= :linear-gradient (:type @state)) "active")}] - #_[:div.gradient-stops - [:div.gradient-background {:style {:background "linear-gradient(90deg, #EC0BE5, #CDCDCD)" }}] - [:div.gradient-stop-wrapper - [:div.gradient-stop.start {:style {:background-color "#EC0BE5"}}] - [:div.gradient-stop.end {:style {:background-color "#CDCDCD" - :left "100%"}}]]] + [:button.gradient.radial-gradient + {:on-click (on-activate-gradient :radial-gradient) + :class (when (= :radial-gradient (:type @state)) "active")}]])] + + [:& gradients {:type (:type @state) + :stops (:stops @state) + :editing-stop (:editing-stop @state) + :on-select-stop handle-change-stop}] + + [:div.colorpicker-tabs + [:div.colorpicker-tab {:class (when (= @active-tab :ramp) "active") + :on-click (change-tab :ramp)} i/picker-ramp] + [:div.colorpicker-tab {:class (when (= @active-tab :harmony) "active") + :on-click (change-tab :harmony)} i/picker-harmony] + [:div.colorpicker-tab {:class (when (= @active-tab :hsva) "active") + :on-click (change-tab :hsva)} i/picker-hsv]] (if picking-color? [:div.picker-detail-wrapper [:div.center-circle] [:canvas#picker-detail {:width 200 :height 160}]] (case @active-tab - :ramp [:& ramp-selector {:color @current-color :on-change handle-change-color}] - :harmony [:& harmony-selector {:color @current-color :on-change handle-change-color}] - :hsva [:& hsva-selector {:color @current-color :on-change handle-change-color}] + :ramp [:& ramp-selector {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color}] + :harmony [:& harmony-selector {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color}] + :hsva [:& hsva-selector {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color}] nil)) - [:& color-inputs {:type (if (= @active-tab :hsva) :hsv :rgb) :color @current-color :on-change handle-change-color}] + [:& color-inputs {:type (if (= @active-tab :hsva) :hsv :rgb) + :disable-opacity disable-opacity + :color current-color + :on-change handle-change-color}] - [:div.libraries - [:select {:on-change (fn [e] - (let [val (-> e dom/get-target dom/get-value)] - (reset! selected-library val))) - :value @selected-library} - [:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")] - [:option {:value "file"} (t locale "workspace.libraries.colors.file-library")] - (for [[_ {:keys [name id]}] shared-libs] - [:option {:key id - :value id} name])] + [:& libraries {:current-color current-color + :disable-gradient disable-gradient + :disable-opacity disable-opacity + :on-select-color on-select-library-color + :on-add-library-color on-add-library-color}] - [:div.selected-colors - (when (= "file" @selected-library) - [:div.color-bullet.button.plus-button {:style {:background-color "white"} - :on-click #(st/emit! (dwl/add-color (:hex @current-color)))} - i/plus]) - - [:div.color-bullet.button {:style {:background-color "white"} - :on-click #(st/emit! (dwc/show-palette (parse-selected @selected-library)))} - i/palette] - - (for [[idx {:keys [id file-id value]}] (map-indexed vector @current-library-colors)] - [:div.color-bullet {:key (str "color-" idx) - :on-click (fn [] - (swap! current-color assoc :hex value) - (reset! value-ref value) - (let [[r g b] (uc/hex->rgb value) - [h s v] (uc/hex->hsv value)] - (swap! current-color assoc - :r r :g g :b b - :h h :s s :v v) - (on-change value (:alpha @current-color) id file-id))) - :style {:background-color value}}])]]] - [:div.colorpicker-tabs - [:div.colorpicker-tab {:class (when (= @active-tab :ramp) "active") - :on-click (change-tab :ramp)} i/picker-ramp] - [:div.colorpicker-tab {:class (when (= @active-tab :harmony) "active") - :on-click (change-tab :harmony)} i/picker-harmony] - [:div.colorpicker-tab {:class (when (= @active-tab :hsva) "active") - :on-click (change-tab :hsva)} i/picker-hsv]] - (when on-accept - [:div.actions - [:button.btn-primary.btn-large - {:on-click (fn [] - (on-accept @value-ref) - (modal/hide!))} - (t locale "workspace.libraries.colors.save-color")]])]) - ) + (when on-accept + [:div.actions + [:button.btn-primary.btn-large + {:on-click (fn [] + (on-accept (state->data @state)) + (modal/hide!))} + (t locale "workspace.libraries.colors.save-color")]])]])) (defn calculate-position "Calculates the style properties for the given coordinates and position" @@ -673,31 +355,32 @@ (mf/defc colorpicker-modal {::mf/register modal/components ::mf/register-as :colorpicker} - [{:keys [x y default value opacity page on-change on-close disable-opacity position on-accept] :as props}] + [{:keys [x y default data page position + disable-gradient + disable-opacity + on-change on-close on-accept] :as props}] (let [vport (mf/deref viewport) dirty? (mf/use-var false) last-change (mf/use-var nil) position (or position :left) style (calculate-position vport position x y) - handle-change (fn [new-value new-opacity id file-id shift-clicked?] - (when (or (not= new-value value) (not= new-opacity opacity)) - (reset! dirty? true)) - (reset! last-change [new-value new-opacity id file-id]) + handle-change (fn [new-data shift-clicked?] + (reset! dirty? (not= data new-data)) + (reset! last-change new-data) (when on-change - (on-change new-value new-opacity id file-id shift-clicked?)))] + (on-change new-data)))] (mf/use-effect (fn [] - #(when (and @dirty? on-close) - (when-let [[value opacity id file-id] @last-change] - (on-close value opacity id file-id))))) + #(when (and @dirty? @last-change on-close) + (on-close @last-change)))) [:div.colorpicker-tooltip {:style (clj->js style)} - [:& colorpicker {:value (or value default) - :opacity (or opacity 1) + [:& colorpicker {:data data + :disable-gradient disable-gradient + :disable-opacity disable-opacity :on-change handle-change - :on-accept on-accept - :disable-opacity disable-opacity}]])) + :on-accept on-accept}]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs new file mode 100644 index 000000000..72a6341bb --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs @@ -0,0 +1,175 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.color-inputs + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [cuerdas.core :as str] + [app.common.geom.point :as gpt] + [app.common.math :as math] + [app.common.uuid :refer [uuid]] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.util.object :as obj] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.colors :as dc] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]])) + +(mf/defc color-inputs [{:keys [type color disable-opacity on-change]}] + (let [{red :r green :g blue :b + hue :h saturation :s value :v + hex :hex alpha :alpha} color + + parse-hex (fn [val] (if (= (first val) \#) val (str \# val))) + + refs {:hex (mf/use-ref nil) + :r (mf/use-ref nil) + :g (mf/use-ref nil) + :b (mf/use-ref nil) + :h (mf/use-ref nil) + :s (mf/use-ref nil) + :v (mf/use-ref nil) + :alpha (mf/use-ref nil)} + + on-change-hex + (fn [e] + (let [val (-> e dom/get-target-val parse-hex)] + (when (uc/hex? val) + (let [[r g b] (uc/hex->rgb val) + [h s v] (uc/hex->hsv hex)] + (on-change {:hex val + :h h :s s :v v + :r r :g g :b b}))))) + + on-change-property + (fn [property max-value] + (fn [e] + (let [val (-> e dom/get-target-val (math/clamp 0 max-value)) + val (if (#{:s} property) (/ val 100) val)] + (when (not (nil? val)) + (if (#{:r :g :b} property) + (let [{:keys [r g b]} (merge color (hash-map property val)) + hex (uc/rgb->hex [r g b]) + [h s v] (uc/hex->hsv hex)] + (on-change {:hex hex + :h h :s s :v v + :r r :g g :b b})) + + (let [{:keys [h s v]} (merge color (hash-map property val)) + hex (uc/hsv->hex [h s v]) + [r g b] (uc/hex->rgb hex)] + (on-change {:hex hex + :h h :s s :v v + :r r :g g :b b}))))))) + + on-change-opacity + (fn [e] + (when-let [new-alpha (-> e dom/get-target-val (math/clamp 0 100) (/ 100))] + (on-change {:alpha new-alpha})))] + + + ;; Updates the inputs values when a property is changed in the parent + (mf/use-effect + (mf/deps color type) + (fn [] + (doseq [ref-key (keys refs)] + (let [property-val (get color ref-key) + property-ref (get refs ref-key)] + (when (and property-val property-ref) + (when-let [node (mf/ref-val property-ref)] + (case ref-key + (:s :alpha) (dom/set-value! node (math/round (* property-val 100))) + :hex (dom/set-value! node property-val) + (dom/set-value! node (math/round property-val))))))))) + + [:div.color-values + {:class (when disable-opacity "disable-opacity")} + [:input {:id "hex-value" + :ref (:hex refs) + :default-value hex + :on-change on-change-hex}] + + (if (= type :rgb) + [:* + [:input {:id "red-value" + :ref (:r refs) + :type "number" + :min 0 + :max 255 + :default-value red + :on-change (on-change-property :r 255)}] + + [:input {:id "green-value" + :ref (:g refs) + :type "number" + :min 0 + :max 255 + :default-value green + :on-change (on-change-property :g 255)}] + + [:input {:id "blue-value" + :ref (:b refs) + :type "number" + :min 0 + :max 255 + :default-value blue + :on-change (on-change-property :b 255)}]] + [:* + [:input {:id "hue-value" + :ref (:h refs) + :type "number" + :min 0 + :max 360 + :default-value hue + :on-change (on-change-property :h 360)}] + + [:input {:id "saturation-value" + :ref (:s refs) + :type "number" + :min 0 + :max 100 + :step 1 + :default-value saturation + :on-change (on-change-property :s 100)}] + + [:input {:id "value-value" + :ref (:v refs) + :type "number" + :min 0 + :max 255 + :default-value value + :on-change (on-change-property :v 255)}]]) + + (when (not disable-opacity) + [:input.alpha-value {:id "alpha-value" + :ref (:alpha refs) + :type "number" + :min 0 + :step 1 + :max 100 + :default-value (if (= alpha :multiple) "" (math/precision alpha 2)) + :on-change on-change-opacity}]) + + [:label.hex-label {:for "hex-value"} "HEX"] + (if (= type :rgb) + [:* + [:label.red-label {:for "red-value"} "R"] + [:label.green-label {:for "green-value"} "G"] + [:label.blue-label {:for "blue-value"} "B"]] + [:* + [:label.red-label {:for "hue-value"} "H"] + [:label.green-label {:for "saturation-value"} "S"] + [:label.blue-label {:for "value-value"} "V"]]) + (when (not disable-opacity) + [:label.alpha-label {:for "alpha-value"} "A"])])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs new file mode 100644 index 000000000..459274b99 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -0,0 +1,54 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.gradients + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [cuerdas.core :as str] + [app.common.geom.point :as gpt] + [app.common.math :as math] + [app.common.uuid :refer [uuid]] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.util.object :as obj] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.colors :as dc] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]])) + +(defn gradient->string [stops] + (let [format-stop + (fn [[offset {:keys [r g b alpha]}]] + (str/fmt "rgba(%s, %s, %s, %s) %s" + r g b alpha (str (* offset 100) "%"))) + + gradient-css (str/join "," (map format-stop stops))] + (str/fmt "linear-gradient(90deg, %s)" gradient-css))) + +(mf/defc gradients [{:keys [type stops editing-stop on-select-stop]}] + (when (#{:linear-gradient :radial-gradient} type) + [:div.gradient-stops + [:div.gradient-background-wrapper + [:div.gradient-background {:style {:background (gradient->string stops)}}]] + + [:div.gradient-stop-wrapper + (for [[offset value] stops] + [:div.gradient-stop + {:class (when (= editing-stop offset) "active") + :on-click (partial on-select-stop offset) + :style {:left (str (* offset 100) "%")}} + + (let [{:keys [hex r g b alpha]} value] + [:* + [:div.gradient-stop-color {:style {:background-color hex}}] + [:div.gradient-stop-alpha {:style {:background-color (str/format "rgba(%s, %s, %s, %s)" r g b alpha)}}]])])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs new file mode 100644 index 000000000..a17a51006 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs @@ -0,0 +1,155 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.harmony + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [cuerdas.core :as str] + [app.common.geom.point :as gpt] + [app.common.math :as math] + [app.common.uuid :refer [uuid]] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.util.object :as obj] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.colors :as dc] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]] + [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]])) + + +(defn create-color-wheel + [canvas-node] + (let [ctx (.getContext canvas-node "2d") + width (obj/get canvas-node "width") + height (obj/get canvas-node "height") + radius (/ width 2) + cx (/ width 2) + cy (/ width 2) + step 0.2] + + (.clearRect ctx 0 0 width height) + + (doseq [degrees (range 0 360 step)] + (let [degrees-rad (math/radians degrees) + x (* radius (math/cos (- degrees-rad))) + y (* radius (math/sin (- degrees-rad)))] + (obj/set! ctx "strokeStyle" (str/format "hsl(%s, 100%, 50%)" degrees)) + (.beginPath ctx) + (.moveTo ctx cx cy) + (.lineTo ctx (+ cx x) (+ cy y)) + (.stroke ctx))) + + (let [grd (.createRadialGradient ctx cx cy 0 cx cx radius)] + (.addColorStop grd 0 "white") + (.addColorStop grd 1 "rgba(255, 255, 255, 0") + (obj/set! ctx "fillStyle" grd) + + (.beginPath ctx) + (.arc ctx cx cy radius 0 (* 2 math/PI) true) + (.closePath ctx) + (.fill ctx)))) + +(defn color->point + [canvas-side hue saturation] + (let [hue-rad (math/radians (- hue)) + comp-x (* saturation (math/cos hue-rad)) + comp-y (* saturation (math/sin hue-rad)) + x (+ (/ canvas-side 2) (* comp-x (/ canvas-side 2))) + y (+ (/ canvas-side 2) (* comp-y (/ canvas-side 2)))] + (gpt/point x y))) + +(mf/defc harmony-selector [{:keys [color disable-opacity on-change]}] + (let [canvas-ref (mf/use-ref nil) + {hue :h saturation :s value :v alpha :alpha} color + + canvas-side 152 + pos-current (color->point canvas-side hue saturation) + pos-complement (color->point canvas-side (mod (+ hue 180) 360) saturation) + dragging? (mf/use-state false) + + calculate-pos (fn [ev] + (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect) + {:keys [x y]} (-> ev dom/get-client-position) + px (math/clamp (/ (- x left) (- right left)) 0 1) + py (math/clamp (/ (- y top) (- bottom top)) 0 1) + + px (- (* 2 px) 1) + py (- (* 2 py) 1) + + angle (math/degrees (math/atan2 px py)) + new-hue (math/precision (mod (- angle 90 ) 360) 2) + new-saturation (math/clamp (math/distance [px py] [0 0]) 0 1) + hex (uc/hsv->hex [new-hue new-saturation value]) + [r g b] (uc/hex->rgb hex)] + (on-change {:hex hex + :r r :g g :b b + :h new-hue + :s new-saturation}))) + + on-change-value (fn [new-value] + (let [hex (uc/hsv->hex [hue saturation new-value]) + [r g b] (uc/hex->rgb hex)] + (on-change {:hex hex + :r r :g g :b b + :v new-value}))) + on-complement-click (fn [ev] + (let [new-hue (mod (+ hue 180) 360) + hex (uc/hsv->hex [new-hue saturation value]) + [r g b] (uc/hex->rgb hex)] + (on-change {:hex hex + :r r :g g :b b + :h new-hue + :s saturation}))) + + on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))] + + (mf/use-effect + (mf/deps canvas-ref) + (fn [] (when canvas-ref + (create-color-wheel (mf/ref-val canvas-ref))))) + + [:div.harmony-selector + [:div.hue-wheel-wrapper + [:canvas.hue-wheel + {:ref canvas-ref + :width canvas-side + :height canvas-side + :on-mouse-down #(reset! dragging? true) + :on-mouse-up #(reset! dragging? false) + :on-pointer-down (partial dom/capture-pointer) + :on-pointer-up (partial dom/release-pointer) + :on-click calculate-pos + :on-mouse-move #(when @dragging? (calculate-pos %))}] + [:div.handler {:style {:pointer-events "none" + :left (:x pos-current) + :top (:y pos-current)}}] + [:div.handler.complement {:style {:left (:x pos-complement) + :top (:y pos-complement) + :cursor "pointer"} + :on-click on-complement-click}]] + [:div.handlers-wrapper + [:& slider-selector {:class "value" + :vertical? true + :reverse? true + :value value + :max-value 255 + :vertical true + :on-change on-change-value}] + (when (not disable-opacity) + [:& slider-selector {:class "opacity" + :vertical? true + :value alpha + :max-value 1 + :vertical true + :on-change on-change-opacity}])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs new file mode 100644 index 000000000..5112f7473 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs @@ -0,0 +1,59 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.hsva + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [cuerdas.core :as str] + [app.common.geom.point :as gpt] + [app.common.math :as math] + [app.common.uuid :refer [uuid]] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.util.object :as obj] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.colors :as dc] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]] + [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]])) + +(mf/defc hsva-selector [{:keys [color disable-opacity on-change]}] + (let [{hue :h saturation :s value :v alpha :alpha} color + handle-change-slider (fn [key] + (fn [new-value] + (let [change (hash-map key new-value) + {:keys [h s v]} (merge color change) + hex (uc/hsv->hex [h s v]) + [r g b] (uc/hex->rgb hex)] + (on-change (merge change + {:hex hex + :r r :g g :b b}))))) + on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))] + [:div.hsva-selector + [:span.hsva-selector-label "H"] + [:& slider-selector + {:class "hue" :max-value 360 :value hue :on-change (handle-change-slider :h)}] + + [:span.hsva-selector-label "S"] + [:& slider-selector + {:class "saturation" :max-value 1 :value saturation :on-change (handle-change-slider :s)}] + + [:span.hsva-selector-label "V"] + [:& slider-selector + {:class "value" :reverse? true :max-value 255 :value value :on-change (handle-change-slider :v)}] + + (when (not disable-opacity) + [:* + [:span.hsva-selector-label "A"] + [:& slider-selector + {:class "opacity" :max-value 1 :value alpha :on-change on-change-opacity}]])])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs new file mode 100644 index 000000000..a28100789 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs @@ -0,0 +1,106 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.libraries + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [cuerdas.core :as str] + [app.common.geom.point :as gpt] + [app.common.math :as math] + [app.common.uuid :refer [uuid]] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.util.object :as obj] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.colors :as dc] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]] + [app.main.ui.components.color-bullet :refer [color-bullet]] + [app.main.ui.workspace.colorpicker.gradients :refer [gradients]] + [app.main.ui.workspace.colorpicker.harmony :refer [harmony-selector]] + [app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]] + [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]] + [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]])) + +(mf/defc libraries [{:keys [current-color on-select-color on-add-library-color + disable-gradient disable-opacity]}] + (let [selected-library (mf/use-state "recent") + current-library-colors (mf/use-state []) + + shared-libs (mf/deref refs/workspace-libraries) + file-colors (mf/deref refs/workspace-file-colors) + recent-colors (mf/deref refs/workspace-recent-colors) + locale (mf/deref i18n/locale) + + parse-selected + (fn [selected] + (if (#{"recent" "file"} selected) + (keyword selected) + (uuid selected)) ) + + check-valid-color? (fn [color] + (and (or (not disable-gradient) (not (:gradient color))) + (or (not disable-opacity) (= 1 (:opacity color)))))] + + ;; Load library colors when the select is changed + (mf/use-effect + (mf/deps @selected-library) + (fn [] + (let [mapped-colors + (cond + (= @selected-library "recent") + ;; The `map?` check is to keep backwards compatibility. We transform from string to map + (map #(if (map? %) % (hash-map :color %)) (reverse (or recent-colors []))) + + (= @selected-library "file") + (vals file-colors) + + :else ;; Library UUID + (map #(merge {:file-id (uuid @selected-library)}) + (vals (get-in shared-libs [(uuid @selected-library) :data :colors]))))] + (reset! current-library-colors (into [] (filter check-valid-color?) mapped-colors))))) + + ;; If the file colors change and the file option is selected updates the state + (mf/use-effect + (mf/deps file-colors) + (fn [] (when (= @selected-library "file") + (let [colors (vals file-colors)] + (reset! current-library-colors (into [] (filter check-valid-color?) colors)))))) + + + [:div.libraries + [:select {:on-change (fn [e] + (when-let [val (dom/get-target-val e)] + (reset! selected-library val))) + :value @selected-library} + [:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")] + [:option {:value "file"} (t locale "workspace.libraries.colors.file-library")] + + (for [[_ {:keys [name id]}] shared-libs] + [:option {:key id + :value id} name])] + + [:div.selected-colors + (when (= "file" @selected-library) + [:div.color-bullet.button.plus-button {:style {:background-color "white"} + :on-click on-add-library-color} + i/plus]) + + [:div.color-bullet.button {:style {:background-color "white"} + :on-click #(st/emit! (dc/show-palette (parse-selected @selected-library)))} + i/palette] + + (for [[idx color] (map-indexed vector @current-library-colors)] + [:& color-bullet {:key (str "color-" idx) + :color color + :on-click #(on-select-color color)}])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/colorpicker/pixel_overlay.cljs new file mode 100644 index 000000000..4f6bb25e8 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/pixel_overlay.cljs @@ -0,0 +1,187 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.pixel-overlay + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [okulary.core :as l] + [promesa.core :as p] + [beicon.core :as rx] + [goog.events :as events] + [app.common.uuid :as uuid] + [app.util.timers :as timers] + [app.util.dom :as dom] + [app.util.object :as obj] + [app.main.data.colors :as dwc] + [app.main.data.fetch :as mdf] + [app.main.data.modal :as modal] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.context :as muc] + [app.main.ui.cursors :as cur] + [app.main.ui.keyboard :as kbd] + [app.main.ui.workspace.shapes :refer [shape-wrapper frame-wrapper]]) + (:import goog.events.EventType)) + +(defn format-viewbox [vbox] + (str/join " " [(+ (:x vbox 0) (:left-offset vbox 0)) + (:y vbox 0) + (:width vbox 0) + (:height vbox 0)])) + +(mf/defc overlay-frames + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [] + (let [data (mf/deref refs/workspace-page) + objects (:objects data) + root (get objects uuid/zero) + shapes (->> (:shapes root) (map #(get objects %)))] + [:* + [:g.shapes + (for [item shapes] + (if (= (:type item) :frame) + [:& frame-wrapper {:shape item + :key (:id item) + :objects objects}] + [:& shape-wrapper {:shape item + :key (:id item)}]))]])) + +(defn draw-picker-canvas [svg-node canvas-node] + (let [canvas-context (.getContext canvas-node "2d") + xml (.serializeToString (js/XMLSerializer.) svg-node) + img-src (str "data:image/svg+xml;base64," + (-> xml js/encodeURIComponent js/unescape js/btoa)) + img (js/Image.) + + on-error (fn [err] (.error js/console "ERROR" err)) + on-load (fn [] (.drawImage canvas-context img 0 0))] + (.addEventListener img "error" on-error) + (.addEventListener img "load" on-load) + (obj/set! img "src" img-src))) + +(mf/defc pixel-overlay + {::mf/wrap-props false} + [props] + (let [vport (unchecked-get props "vport") + vbox (unchecked-get props "vbox") + viewport-ref (unchecked-get props "viewport-ref") + options (unchecked-get props "options") + svg-ref (mf/use-ref nil) + canvas-ref (mf/use-ref nil) + fetch-pending (mf/deref (mdf/pending-ref)) + + update-canvas-stream (rx/subject) + + handle-keydown + (fn [event] + (when (and (kbd/esc? event)) + (do (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (dwc/stop-picker)) + (modal/disallow-click-outside!)))) + + on-mouse-move-picker + (fn [event] + (when-let [zoom-view-node (.getElementById js/document "picker-detail")] + (let [{brx :left bry :top} (dom/get-bounding-rect (mf/ref-val viewport-ref)) + x (- (.-clientX event) brx) + y (- (.-clientY event) bry) + + zoom-context (.getContext zoom-view-node "2d") + canvas-node (mf/ref-val canvas-ref) + canvas-context (.getContext canvas-node "2d") + pixel-data (.getImageData canvas-context x y 1 1) + rgba (.-data pixel-data) + r (obj/get rgba 0) + g (obj/get rgba 1) + b (obj/get rgba 2) + a (obj/get rgba 3) + + area-data (.getImageData canvas-context (- x 25) (- y 20) 50 40)] + + (-> (js/createImageBitmap area-data) + (p/then (fn [image] + ;; Draw area + (obj/set! zoom-context "imageSmoothingEnabled" false) + (.drawImage zoom-context image 0 0 200 160)))) + (st/emit! (dwc/pick-color [r g b a]))))) + + on-mouse-down-picker + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (dwc/pick-color-select true (kbd/shift? event)))) + + on-mouse-up-picker + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (dwc/stop-picker)) + (modal/disallow-click-outside!))] + + (mf/use-effect + (fn [] + (let [listener (events/listen js/document EventType.KEYDOWN handle-keydown)] + #(events/unlistenByKey listener)))) + + (mf/use-effect + (fn [] + (let [sub (->> update-canvas-stream + (rx/debounce 10) + (rx/subs #(draw-picker-canvas (mf/ref-val svg-ref) + (mf/ref-val canvas-ref))))] + + #(rx/dispose! sub)))) + + (mf/use-effect + (mf/deps svg-ref canvas-ref) + (fn [] + (when (and svg-ref canvas-ref) + + (let [config (clj->js {:attributes true + :childList true + :subtree true + :characterData true}) + on-svg-change (fn [mutation-list] (rx/push! update-canvas-stream :update)) + observer (js/MutationObserver. on-svg-change)] + + (.observe observer (mf/ref-val svg-ref) config) + + ;; Disconnect on unmount + #(.disconnect observer))))) + + [:* + [:div.overlay + {:tab-index 0 + :style {:position "absolute" + :top 0 + :left 0 + :width "100%" + :height "100%" + :cursor cur/picker} + :on-mouse-down on-mouse-down-picker + :on-mouse-up on-mouse-up-picker + :on-mouse-move on-mouse-move-picker}] + [:canvas {:ref canvas-ref + :width (:width vport 0) + :height (:height vport 0) + :style {:display "none"}}] + + [:& (mf/provider muc/embed-ctx) {:value true} + [:svg.viewport + {:ref svg-ref + :preserveAspectRatio "xMidYMid meet" + :width (:width vport 0) + :height (:height vport 0) + :view-box (format-viewbox vbox) + :style {:display "none" + :background-color (get options :background "#E8E9EA")}} + [:& overlay-frames]]]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/pixel_picker.cljs b/frontend/src/app/main/ui/workspace/colorpicker/pixel_picker.cljs new file mode 100644 index 000000000..21a35a0fc --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/pixel_picker.cljs @@ -0,0 +1,29 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.pixel-picker + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [cuerdas.core :as str] + [app.common.geom.point :as gpt] + [app.common.math :as math] + [app.common.uuid :refer [uuid]] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.util.object :as obj] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.colors :as dc] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]])) + + diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs new file mode 100644 index 000000000..f88794bab --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs @@ -0,0 +1,95 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.ramp + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [cuerdas.core :as str] + [app.common.geom.point :as gpt] + [app.common.math :as math] + [app.common.uuid :refer [uuid]] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.util.object :as obj] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.colors :as dc] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]] + [app.main.ui.components.color-bullet :refer [color-bullet]] + [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]])) + +(mf/defc value-saturation-selector [{:keys [hue saturation value on-change]}] + (let [dragging? (mf/use-state false) + calculate-pos + (fn [ev] + (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect) + {:keys [x y]} (-> ev dom/get-client-position) + px (math/clamp (/ (- x left) (- right left)) 0 1) + py (* 255 (- 1 (math/clamp (/ (- y top) (- bottom top)) 0 1)))] + (on-change px py)))] + [:div.value-saturation-selector + {:on-mouse-down #(reset! dragging? true) + :on-mouse-up #(reset! dragging? false) + :on-pointer-down (partial dom/capture-pointer) + :on-pointer-up (partial dom/release-pointer) + :on-click calculate-pos + :on-mouse-move #(when @dragging? (calculate-pos %))} + [:div.handler {:style {:pointer-events "none" + :left (str (* 100 saturation) "%") + :top (str (* 100 (- 1 (/ value 255))) "%")}}]])) + + +(mf/defc ramp-selector [{:keys [color disable-opacity on-change]}] + (let [{hex :hex + hue :h saturation :s value :v alpha :alpha} color + + on-change-value-saturation + (fn [new-saturation new-value] + (let [hex (uc/hsv->hex [hue new-saturation new-value]) + [r g b] (uc/hex->rgb hex)] + (on-change {:hex hex + :r r :g g :b b + :s new-saturation + :v new-value}))) + + on-change-hue + (fn [new-hue] + (let [hex (uc/hsv->hex [new-hue saturation value]) + [r g b] (uc/hex->rgb hex)] + (on-change {:hex hex + :r r :g g :b b + :h new-hue} ))) + + on-change-opacity + (fn [new-opacity] + (on-change {:alpha new-opacity} ))] + [:* + [:& value-saturation-selector + {:hue hue + :saturation saturation + :value value + :on-change on-change-value-saturation}] + + [:div.shade-selector + [:& color-bullet {:color {:color hex + :opacity alpha}}] + [:& slider-selector {:class "hue" + :max-value 360 + :value hue + :on-change on-change-hue}] + + (when (not disable-opacity) + [:& slider-selector {:class "opacity" + :max-value 1 + :value alpha + :on-change on-change-opacity}])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs new file mode 100644 index 000000000..e6b4e0c4f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs @@ -0,0 +1,68 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.colorpicker.slider-selector + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [cuerdas.core :as str] + [app.common.geom.point :as gpt] + [app.common.math :as math] + [app.common.uuid :refer [uuid]] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.util.object :as obj] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.colors :as dc] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]])) + +(mf/defc slider-selector + [{:keys [value class min-value max-value vertical? reverse? on-change]}] + (let [min-value (or min-value 0) + max-value (or max-value 1) + dragging? (mf/use-state false) + calculate-pos + (fn [ev] + (when on-change + (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect) + {:keys [x y]} (-> ev dom/get-client-position) + unit-value (if vertical? + (math/clamp (/ (- bottom y) (- bottom top)) 0 1) + (math/clamp (/ (- x left) (- right left)) 0 1)) + unit-value (if reverse? + (math/abs (- unit-value 1.0)) + unit-value) + value (+ min-value (* unit-value (- max-value min-value)))] + (on-change (math/precision value 2)))))] + + [:div.slider-selector + {:class (str (if vertical? "vertical " "") class) + :on-mouse-down #(reset! dragging? true) + :on-mouse-up #(reset! dragging? false) + :on-pointer-down (partial dom/capture-pointer) + :on-pointer-up (partial dom/release-pointer) + :on-click calculate-pos + :on-mouse-move #(when @dragging? (calculate-pos %))} + + (let [value-percent (* (/ (- value min-value) + (- max-value min-value)) 100) + + value-percent (if reverse? + (math/abs (- value-percent 100)) + value-percent) + value-percent-str (str value-percent "%") + + style-common #js {:pointerEvents "none"} + style-horizontal (obj/merge! #js {:left value-percent-str} style-common) + style-vertical (obj/merge! #js {:bottom value-percent-str} style-common)] + [:div.handler {:style (if vertical? style-vertical style-horizontal)}])])) diff --git a/frontend/src/app/main/ui/workspace/frame_grid.cljs b/frontend/src/app/main/ui/workspace/frame_grid.cljs index 969a55414..88b200096 100644 --- a/frontend/src/app/main/ui/workspace/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/frame_grid.cljs @@ -18,7 +18,9 @@ (mf/defc square-grid [{:keys [frame zoom grid] :as props}] (let [{:keys [color size] :as params} (-> grid :params) - {color-value :value color-opacity :opacity} (-> grid :params :color) + {color-value :color color-opacity :opacity} (-> grid :params :color) + ;; Support for old color format + color-value (or color-value (:value (get-in grid [:params :color :value]))) {frame-width :width frame-height :height :keys [x y]} frame] (when (> size 0) [:g.grid @@ -43,7 +45,9 @@ :stroke-width (str (/ 1 zoom))}}])]]))) (mf/defc layout-grid [{:keys [key frame zoom grid]}] - (let [{color-value :value color-opacity :opacity} (-> grid :params :color) + (let [{color-value :color color-opacity :opacity} (-> grid :params :color) + ;; Support for old color format + color-value (or color-value (:value (get-in grid [:params :color :value]))) gutter (-> grid :params :gutter) gutter? (and (not (nil? gutter)) (not= gutter 0)) diff --git a/frontend/src/app/main/ui/workspace/gradients.cljs b/frontend/src/app/main/ui/workspace/gradients.cljs index 0fc1e92a8..007b8631d 100644 --- a/frontend/src/app/main/ui/workspace/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/gradients.cljs @@ -13,13 +13,17 @@ [rumext.alpha :as mf] [cuerdas.core :as str] [beicon.core :as rx] - [app.main.data.workspace.common :as dwc] - [app.main.store :as st] - [app.main.streams :as ms] + [okulary.core :as l] [app.common.math :as mth] - [app.util.dom :as dom] [app.common.geom.point :as gpt] - [app.common.geom.matrix :as gmt])) + [app.common.geom.matrix :as gmt] + [app.util.dom :as dom] + [app.main.store :as st] + [app.main.refs :as refs] + [app.main.streams :as ms] + [app.main.data.modal :as modal] + [app.main.data.workspace.common :as dwc] + [app.main.data.colors :as dc])) (def gradient-line-stroke-width 2) (def gradient-line-stroke-color "white") @@ -31,6 +35,12 @@ (def gradient-square-stroke-color "white") (def gradient-square-stroke-color-selected "#1FDEA7") +(def editing-spot-ref + (l/derived (l/in [:workspace-local :editing-stop]) st/state)) + +(def current-gradient-ref + (l/derived (l/in [:workspace-local :current-gradient]) st/state)) + (mf/defc shadow [{:keys [id x y width height offset]}] [:filter {:id id :x x @@ -77,24 +87,13 @@ :height (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4) :offset (/ 2 zoom)}]) -(def default-gradient - {:type :linear - :start-x 0.5 :start-y 0.5 - :end-x 0.5 :end-y 1 - :width 1.0 - :stops [{:offset 0 - :color "#FF0000" - :opacity 1} - {:offset 1 - :color "#FF0000" - :opacity 0.2}]}) - (def checkboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAACvUlEQVQoFQGyAk39AeLi4gAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB////AAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjScaa0cU7nIAAAAASUVORK5CYII=") #_(def checkboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") (mf/defc gradient-color-handler - [{:keys [filter-id zoom point color angle on-click on-mouse-down on-mouse-up]}] + [{:keys [filter-id zoom point color angle selected + on-click on-mouse-down on-mouse-up]}] [:g {:filter (str/fmt "url(#%s)" filter-id) :transform (gmt/rotate-matrix angle point)} @@ -114,12 +113,13 @@ :on-mouse-down (partial on-mouse-down :to-p) :on-mouse-up (partial on-mouse-up :to-p)}] - [:rect {:x (- (:x point) (/ gradient-square-width 2 zoom)) + [:rect {:data-allow-click-modal "colorpicker" + :x (- (:x point) (/ gradient-square-width 2 zoom)) :y (- (:y point) (/ gradient-square-width 2 zoom)) :rx (/ gradient-square-radius zoom) :width (/ gradient-square-width zoom) :height (/ gradient-square-width zoom) - :stroke "white" + :stroke (if selected "#31EFB8" "white") :stroke-width (/ gradient-square-stroke-width zoom) :fill (:value color) :fill-opacity (:opacity color) @@ -128,18 +128,27 @@ :on-mouse-up on-mouse-up}]]) (mf/defc gradient-handler-transformed - [{:keys [from-p to-p width-p from-color to-color zoom on-change-start on-change-finish on-change-width on-change-stop-color]}] + [{:keys [from-p to-p width-p from-color to-color zoom editing + on-change-start on-change-finish on-change-width on-change-stop-color]}] (let [moving-point (mf/use-var nil) angle (+ 90 (gpt/angle from-p to-p)) on-click (fn [position event] (dom/stop-propagation event) - (dom/prevent-default event)) + (dom/prevent-default event) + (when (#{:from-p :to-p} position) + (st/emit! (dc/select-gradient-stop (case position + :from-p 0 + :to-p 1))))) on-mouse-down (fn [position event] (dom/stop-propagation event) (dom/prevent-default event) - (reset! moving-point position)) + (reset! moving-point position) + (when (#{:from-p :to-p} position) + (st/emit! (dc/select-gradient-stop (case position + :from-p 0 + :to-p 1))))) on-mouse-up (fn [position event] (dom/stop-propagation event) @@ -192,7 +201,8 @@ (when width-p [:g {:filter "url(#gradient_width_handler_drop_shadow)"} - [:circle {:cx (:x width-p) + [:circle {:data-allow-click-modal "colorpicker" + :cx (:x width-p) :cy (:y width-p) :r (/ gradient-width-handler-radius zoom) :fill gradient-width-handler-color @@ -200,7 +210,8 @@ :on-mouse-up (partial on-mouse-up :width-p)}]]) [:& gradient-color-handler - {:filter-id "gradient_square_from_drop_shadow" + {:selected (or (not editing) (= editing 0)) + :filter-id "gradient_square_from_drop_shadow" :zoom zoom :point from-p :color from-color @@ -210,7 +221,8 @@ :on-mouse-up (partial on-mouse-up :from-p)}] [:& gradient-color-handler - {:filter-id "gradient_square_to_drop_shadow" + {:selected (= editing 1) + :filter-id "gradient_square_to_drop_shadow" :zoom zoom :point to-p :color to-color @@ -219,74 +231,70 @@ :on-mouse-down (partial on-mouse-down :to-p) :on-mouse-up (partial on-mouse-up :to-p)}]])) -(mf/defc gradient-handlers - [{:keys [shape zoom]}] - (let [{:keys [x y width height] :as sr} (:selrect shape) - state (mf/use-state (:fill-color-gradient shape default-gradient)) +(mf/defc gradient-handlers + [{:keys [id zoom]}] + (let [shape (mf/deref (refs/object-by-id id)) + gradient (mf/deref current-gradient-ref) + editing-spot (mf/deref editing-spot-ref) + + {:keys [x y width height] :as sr} (:selrect shape) [{start-color :color start-opacity :opacity} - {end-color :color end-opacity :opacity}] (:stops @state) + {end-color :color end-opacity :opacity}] (:stops gradient) - from-p (gpt/point (+ x (* width (:start-x @state))) - (+ y (* height (:start-y @state)))) + from-p (gpt/point (+ x (* width (:start-x gradient))) + (+ y (* height (:start-y gradient)))) - to-p (gpt/point (+ x (* width (:end-x @state))) - (+ y (* height (:end-y @state)))) + to-p (gpt/point (+ x (* width (:end-x gradient))) + (+ y (* height (:end-y gradient)))) gradient-vec (gpt/to-vec from-p to-p) gradient-length (gpt/length gradient-vec) width-v (-> gradient-vec (gpt/normal-left) - (gpt/multiply (gpt/point (* (:width @state) (/ gradient-length (/ height 2) )))) + (gpt/multiply (gpt/point (* (:width gradient) (/ gradient-length (/ height 2) )))) (gpt/multiply (gpt/point (/ width 2)))) width-p (gpt/add from-p width-v) + change! (fn [changes] + (st/emit! (dc/update-gradient changes))) + on-change-start (fn [point] (let [start-x (/ (- (:x point) x) width) - start-y (/ (- (:y point) y) height)] - (swap! state assoc - :start-x start-x - :start-y start-y ))) + start-y (/ (- (:y point) y) height) + start-x (mth/precision start-x 2) + start-y (mth/precision start-y 2)] + (change! {:start-x start-x :start-y start-y}))) on-change-finish (fn [point] (let [end-x (/ (- (:x point) x) width) - end-y (/ (- (:y point) y) height)] - (swap! state assoc - :end-x end-x - :end-y end-y))) + end-y (/ (- (:y point) y) height) + + end-x (mth/precision end-x 2) + end-y (mth/precision end-y 2)] + (change! {:end-x end-x :end-y end-y}))) on-change-width (fn [point] (let [scale-factor-y (/ gradient-length (/ height 2)) norm-dist (/ (gpt/distance point from-p) (* (/ width 2) scale-factor-y))] - (swap! state assoc :width norm-dist))) - on-change-stop-color (fn [offset color opacity] (println "change-color"))] + (change! {:width norm-dist})))] - (mf/use-effect - (mf/deps shape) - (fn [] - (reset! state (:fill-color-gradient shape default-gradient)))) - - (mf/use-effect - (mf/deps @state) - (fn [] - (when (not= (:fill-color-gradient shape) @state) - (st/emit! (dwc/update-shapes - [(:id shape)] - #(assoc % :fill-color-gradient @state)))))) - - [:& gradient-handler-transformed - {:from-p from-p - :to-p to-p - :width-p (when (= :radial (:type @state)) width-p) - :from-color {:value start-color :opacity start-opacity} - :to-color {:value end-color :opacity end-opacity} - :zoom zoom - :on-change-start on-change-start - :on-change-finish on-change-finish - :on-change-width on-change-width - :on-change-stop-color on-change-stop-color}])) + (when (and gradient + (= id (:shape-id gradient)) + (not= (:type shape) :text)) + [:& gradient-handler-transformed + {:editing editing-spot + :from-p from-p + :to-p to-p + :width-p (when (= :radial (:type gradient)) width-p) + :from-color {:value start-color :opacity start-opacity} + :to-color {:value end-color :opacity end-opacity} + :zoom zoom + :on-change-start on-change-start + :on-change-finish on-change-finish + :on-change-width on-change-width}]))) diff --git a/frontend/src/app/main/ui/workspace/selection.cljs b/frontend/src/app/main/ui/workspace/selection.cljs index b5e281510..eda6c1888 100644 --- a/frontend/src/app/main/ui/workspace/selection.cljs +++ b/frontend/src/app/main/ui/workspace/selection.cljs @@ -28,8 +28,7 @@ [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] [app.util.debug :refer [debug?]] - [app.main.ui.workspace.shapes.outline :refer [outline]] - [app.main.ui.workspace.gradients :refer [gradient-handlers]])) + [app.main.ui.workspace.shapes.outline :refer [outline]])) (def rotation-handler-size 25) (def resize-point-radius 4) @@ -210,11 +209,7 @@ (case type :rotation (when (not= :frame (:type shape)) [:> rotation-handler props]) :resize-point [:> resize-point-handler props] - :resize-side [:> resize-side-handler props]))) - - #_(when (= :rect (:type shape)) - [:& gradient-handlers {:shape tr-shape - :zoom zoom}])]))) + :resize-side [:> resize-side-handler props])))]))) ;; --- Selection Handlers (Component) (mf/defc path-edition-selection-handlers diff --git a/frontend/src/app/main/ui/workspace/shapes/common.cljs b/frontend/src/app/main/ui/workspace/shapes/common.cljs index 2df882f9e..302c0cf48 100644 --- a/frontend/src/app/main/ui/workspace/shapes/common.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/common.cljs @@ -14,11 +14,11 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.keyboard :as kbd] - [app.main.ui.shapes.filters :as filters] [app.util.dom :as dom] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom])) + [app.common.geom.shapes :as geom] + [app.main.ui.shapes.shape :refer [shape-container]])) (defn- on-mouse-down [event {:keys [id type] :as shape}] @@ -47,7 +47,7 @@ (st/emit! (dw/select-shape id true))) (do (when-not (or (empty? selected) (kbd/shift? event)) - (st/emit! dw/deselect-all)) + (st/emit! (dw/deselect-all))) (st/emit! (dw/select-shape id)))) (st/emit! (dw/start-move-selected))))))) @@ -70,12 +70,11 @@ #(on-mouse-down % shape)) on-context-menu (mf/use-callback (mf/deps shape) - #(on-context-menu % shape)) - filter-id (mf/use-memo filters/get-filter-id)] - [:g.shape {:on-mouse-down on-mouse-down - :on-context-menu on-context-menu - :filter (filters/filter-str filter-id shape)} - [:& filters/filters {:filter-id filter-id :shape shape}] + #(on-context-menu % shape))] + + [:> shape-container {:shape shape + :on-mouse-down on-mouse-down + :on-context-menu on-context-menu} [:& component {:shape shape}]]))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 1b3b46965..d72bc71ca 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -19,13 +19,13 @@ [app.main.ui.workspace.shapes.common :as common] [app.main.data.workspace.selection :as dws] [app.main.ui.shapes.frame :as frame] - [app.main.ui.shapes.filters :as filters] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] [app.util.dom :as dom] [app.main.streams :as ms] - [app.util.timers :as ts])) + [app.util.timers :as ts] + [app.main.ui.shapes.shape :refer [shape-container]])) (defn- frame-wrapper-factory-equals? [np op] @@ -99,7 +99,7 @@ (mf/deps (:id shape)) (fn [event] (dom/prevent-default event) - (st/emit! dw/deselect-all + (st/emit! (dw/deselect-all) (dw/select-shape (:id shape))))) on-mouse-over @@ -112,9 +112,7 @@ (mf/use-callback (mf/deps (:id shape)) (fn [] - (st/emit! (dws/change-hover-state (:id shape) false)))) - - filter-id (mf/use-memo filters/get-filter-id)] + (st/emit! (dws/change-hover-state (:id shape) false))))] (when-not (:hidden shape) [:g {:class (when selected? "selected") @@ -126,8 +124,8 @@ :on-context-menu on-context-menu :on-double-click on-double-click :on-mouse-down on-mouse-down}] - [:g.frame {:filter (filters/filter-str filter-id shape)} - [:& filters/filters {:filter-id filter-id :shape shape}] + + [:> shape-container {:shape shape} [:& frame-shape {:shape shape :childs children}]]]))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/interactions.cljs b/frontend/src/app/main/ui/workspace/shapes/interactions.cljs index fb36edad8..d4a8684fe 100644 --- a/frontend/src/app/main/ui/workspace/shapes/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/interactions.cljs @@ -32,7 +32,7 @@ (do (dom/stop-propagation event) (when-not (empty? selected) - (st/emit! dw/deselect-all)) + (st/emit! (dw/deselect-all))) (st/emit! (dw/select-shape id)) (st/emit! (dw/start-create-interaction)))) diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index d128509aa..81fd43816 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -11,18 +11,19 @@ (:require [rumext.alpha :as mf] [app.common.data :as d] + [app.util.dom :as dom] + [app.util.timers :as ts] + [app.main.streams :as ms] [app.main.constants :as c] - [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] + [app.main.data.workspace :as dw] + [app.main.data.workspace.drawing :as dr] [app.main.ui.keyboard :as kbd] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.filters :as filters] - [app.main.ui.workspace.shapes.common :as common] - [app.main.data.workspace.drawing :as dr] - [app.util.dom :as dom] - [app.main.streams :as ms] - [app.util.timers :as ts])) + [app.main.ui.shapes.shape :refer [shape-container]] + [app.main.ui.workspace.shapes.common :as common])) (mf/defc path-wrapper {::mf/wrap-props false} @@ -42,13 +43,13 @@ (do (dom/stop-propagation event) (dom/prevent-default event) - (st/emit! (dw/start-edition-mode (:id shape))))))) - filter-id (mf/use-memo filters/get-filter-id)] + (st/emit! (dw/start-edition-mode (:id shape)))))))] - [:g.shape {:on-double-click on-double-click - :on-mouse-down on-mouse-down - :on-context-menu on-context-menu - :filter (filters/filter-str filter-id shape)} - [:& filters/filters {:filter-id filter-id :shape shape}] - [:& path/path-shape {:shape shape :background? true}]])) + [:> shape-container {:shape shape + :on-double-click on-double-click + :on-mouse-down on-mouse-down + :on-context-menu on-context-menu} + + [:& path/path-shape {:shape shape + :background? true}]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 3f22cbf7f..1fccaefdd 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -9,32 +9,34 @@ (ns app.main.ui.workspace.shapes.text (:require - [cuerdas.core :as str] + ["slate" :as slate] + ["slate-react" :as rslate] [goog.events :as events] [goog.object :as gobj] + [cuerdas.core :as str] [rumext.alpha :as mf] + [beicon.core :as rx] + [app.util.color :as color] + [app.util.dom :as dom] + [app.util.text :as ut] + [app.util.object :as obj] + [app.util.color :as uc] + [app.util.timers :as timers] [app.common.data :as d] + [app.common.geom.shapes :as geom] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.fonts :as fonts] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.texts :as dwt] - [app.main.refs :as refs] - [app.main.store :as st] [app.main.ui.cursors :as cur] [app.main.ui.workspace.shapes.common :as common] [app.main.ui.shapes.text :as text] [app.main.ui.keyboard :as kbd] [app.main.ui.context :as muc] [app.main.ui.shapes.filters :as filters] - [app.main.fonts :as fonts] - [app.util.color :as color] - [app.util.dom :as dom] - [app.util.text :as ut] - [app.common.geom.shapes :as geom] - [app.util.object :as obj] - [app.util.color :as uc] - [app.util.timers :as timers] - ["slate" :as slate] - ["slate-react" :as rslate]) + [app.main.ui.shapes.shape :refer [shape-container]]) (:import goog.events.EventType goog.events.KeyCodes)) @@ -78,9 +80,7 @@ (dom/stop-propagation event) (dom/prevent-default event) (when selected? - (st/emit! (dw/start-edition-mode (:id shape))))) - - filter-id (mf/use-memo filters/get-filter-id)] + (st/emit! (dw/start-edition-mode (:id shape)))))] (mf/use-effect (mf/deps shape edition selected? current-transform) @@ -88,26 +88,25 @@ selected? (not edition?) (not embed-resources?) - (nil? current-transform))] - (timers/schedule #(reset! render-editor check?))))) + (nil? current-transform)) + result (timers/schedule #(reset! render-editor check?))] + #(rx/dispose! result)))) - [:g.shape {:on-double-click on-double-click - :on-mouse-down on-mouse-down - :on-context-menu on-context-menu - :filter (filters/filter-str filter-id shape)} - [:& filters/filters {:filter-id filter-id :shape shape}] - [:* - (when @render-editor - [:g {:opacity 0 - :style {:pointer-events "none"}} - ;; We only render the component for its side-effect - [:& text-shape-edit {:shape shape - :read-only? true}]]) + [:> shape-container {:shape shape + :on-double-click on-double-click + :on-mouse-down on-mouse-down + :on-context-menu on-context-menu} + (when @render-editor + [:g {:opacity 0 + :style {:pointer-events "none"}} + ;; We only render the component for its side-effect + [:& text-shape-edit {:shape shape + :read-only? true}]]) - (if edition? - [:& text-shape-edit {:shape shape}] - [:& text/text-shape {:shape shape - :selected? selected?}])]])) + (if edition? + [:& text-shape-edit {:shape shape}] + [:& text/text-shape {:shape shape + :selected? selected?}])])) ;; --- Text Editor Rendering @@ -158,17 +157,25 @@ fill-color (obj/get data "fill-color" fill) fill-opacity (obj/get data "fill-opacity" opacity) + fill-color-gradient (obj/get data "fill-color-gradient" nil) + fill-color-gradient (when fill-color-gradient + (-> (js->clj fill-color-gradient :keywordize-keys true) + (update :type keyword))) + fill-color-ref-id (obj/get data "fill-color-ref-id") fill-color-ref-file (obj/get data "fill-color-ref-file") [r g b a] (uc/hex->rgba fill-color fill-opacity) + background (if fill-color-gradient + (uc/gradient->css (js->clj fill-color-gradient)) + (str/format "rgba(%s, %s, %s, %s)" r g b a)) fontsdb (deref fonts/fontsdb) base #js {:textDecoration text-decoration - :color (str/format "rgba(%s, %s, %s, %s)" r g b a) :textTransform text-transform - :lineHeight (or line-height "inherit")}] + :lineHeight (or line-height "inherit") + "--text-color" background}] (when (and (string? letter-spacing) (pos? (alength letter-spacing))) @@ -243,7 +250,9 @@ childs (obj/get props "children") data (obj/get props "leaf") style (generate-text-styles data) - attrs (obj/set! attrs "style" style)] + attrs (-> attrs + (obj/set! "style" style) + (obj/set! "className" "text-node"))] [:> :span attrs childs])) (defn- render-element @@ -284,6 +293,14 @@ children-count (->> node :children (map content-size) (reduce +))] (+ current children-count))) +(defn fix-gradients + "Fix for the gradient types that need to be keywords" + [content] + (let [fix-node + (fn [node] + (d/update-in-when node [:fill-color-gradient :type] keyword))] + (ut/map-node fix-node content))) + (mf/defc text-shape-edit {::mf/wrap [mf/memo]} [{:keys [shape read-only?] :or {read-only? false} :as props}] @@ -364,7 +381,8 @@ (fn [val] (when (not read-only?) (let [content (js->clj val :keywordize-keys true) - content (first content)] + content (first content) + content (fix-gradients content)] ;; Append timestamp so we can react to cursor change events (st/emit! (dw/update-shape id {:content (assoc content :ts (js->clj (.now js/Date)))})) (reset! state val) @@ -419,7 +437,8 @@ :x x :y y :width (if (= :auto-width grow-type) 10000 width) :height height} - [:style "span { line-height: inherit; }"] + [:style "span { line-height: inherit; } + .text-node { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] [:> rslate/Slate {:editor editor :value @state :on-change on-change} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 1abd0432a..a4930e09b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -28,6 +28,7 @@ [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.workspace.sidebar.options.typography :refer [typography-entry]] + [app.main.ui.components.color-bullet :as bc] [app.main.ui.icons :as i] [app.main.ui.keyboard :as kbd] [app.main.data.modal :as modal] @@ -190,7 +191,7 @@ :options [[(tr "workspace.assets.delete") on-delete]]}])])])) (mf/defc color-item - [{:keys [color local? locale file-id] :as props}] + [{:keys [color local? locale] :as props}] (let [rename? (= (:color-for-rename @refs/workspace-local) (:id color)) id (:id color) input-ref (mf/use-ref) @@ -198,20 +199,27 @@ :top nil :left nil :editing rename?}) + + default-name (cond + (:gradient color) (bc/gradient-type->string (get-in color [:gradient :type])) + (:color color) (:color color) + :else (:value color)) + click-color (fn [event] (let [ids (get-in @st/state [:workspace-local :selected])] (if (kbd/shift? event) - (st/emit! (dc/change-stroke ids (:value color) id (if local? nil file-id))) - (st/emit! (dc/change-fill ids (:value color) id (if local? nil file-id)))))) + (st/emit! (dc/change-stroke ids color)) + (st/emit! (dc/change-fill ids color))))) rename-color (fn [name] (st/emit! (dwl/update-color (assoc color :name name)))) edit-color - (fn [value] - (st/emit! (dwl/update-color (assoc color :value value)))) + (fn [new-color] + (let [updated-color (merge new-color (select-keys color [:id :file-id :name]))] + (st/emit! (dwl/update-color updated-color)))) delete-color (fn [] @@ -245,8 +253,7 @@ {:x (.-clientX event) :y (.-clientY event) :on-accept edit-color - :value (:value color) - :disable-opacity true + :data color :position :right})) on-context-menu @@ -269,7 +276,8 @@ nil)) [:div.group-list-item {:on-context-menu on-context-menu} - [:div.color-block {:style {:background-color (:value color)}}] + [:& bc/color-bullet {:color color}] + (if (:editing @state) [:input.element-name {:type "text" @@ -278,12 +286,13 @@ :on-key-down input-key-down :auto-focus true :default-value (:name color "")}] + [:div.name-block {:on-double-click rename-color-clicked :on-click click-color} (:name color) - (when-not (= (:name color) (:value color)) - [:span (:value color)])]) + (when-not (= (:name color) default-name) + [:span default-name])]) (when local? [:& context-menu {:selectable false @@ -312,8 +321,8 @@ {:x (.-clientX event) :y (.-clientY event) :on-accept add-color - :value "#406280" - :disable-opacity true + :data {:color "#406280" + :opacity 1} :position :right})))] [:div.asset-group [:div.group-title {:class (when (not open?) "closed")} @@ -324,11 +333,14 @@ (when open? [:div.group-list (for [color colors] - [:& color-item {:key (:id color) - :color color - :file-id file-id - :local? local? - :locale locale}])])])) + (let [color (cond-> color + (:value color) (assoc :color (:value color) :opacity 1) + (:value color) (dissoc :value) + true (assoc :file-id file-id))] + [:& color-item {:key (:id color) + :color color + :local? local? + :locale locale}]))])])) (mf/defc typography-box [{:keys [file file-id local? typographies locale open? on-open on-close] :as props}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 889d04dcd..5080656f5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -143,10 +143,10 @@ (st/emit! (dw/select-shape id true)) (> (count selected) 1) - (st/emit! dw/deselect-all + (st/emit! (dw/deselect-all) (dw/select-shape id)) :else - (st/emit! dw/deselect-all + (st/emit! (dw/deselect-all) (dw/select-shape id))))) on-context-menu @@ -160,7 +160,7 @@ on-drag (fn [{:keys [id]}] (when (not (contains? selected id)) - (st/emit! dw/deselect-all + (st/emit! (dw/deselect-all) (dw/select-shape id)))) on-drop diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 2bfe53b19..bca5a51de 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -40,15 +40,15 @@ [{:keys [shape shapes-with-children page-id file-id]}] [:* (case (:type shape) - :frame [:& frame/options {:shape shape}] - :group [:& group/options {:shape shape :shape-with-children shapes-with-children}] - :text [:& text/options {:shape shape}] - :rect [:& rect/options {:shape shape}] - :icon [:& icon/options {:shape shape}] + :frame [:& frame/options {:shape shape}] + :group [:& group/options {:shape shape :shape-with-children shapes-with-children}] + :text [:& text/options {:shape shape}] + :rect [:& rect/options {:shape shape}] + :icon [:& icon/options {:shape shape}] :circle [:& circle/options {:shape shape}] - :path [:& path/options {:shape shape}] - :curve [:& path/options {:shape shape}] - :image [:& image/options {:shape shape}] + :path [:& path/options {:shape shape}] + :curve [:& path/options {:shape shape}] + :image [:& image/options {:shape shape}] nil) [:& exports-menu {:shape shape diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs index 511ebcdfa..b2b879a7e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs @@ -21,7 +21,7 @@ [app.util.i18n :as i18n :refer [tr t]] [app.util.object :as obj])) -(def fill-attrs [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file]) +(def fill-attrs [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient]) (defn- fill-menu-props-equals? [np op] @@ -36,42 +36,47 @@ (= (:fill-color new-values) (:fill-color old-values)) (= (:fill-opacity new-values) - (:fill-opacity old-values))))) + (:fill-opacity old-values)) + (= (:fill-color-gradient new-values) + (:fill-color-gradient old-values))))) (mf/defc fill-menu {::mf/wrap [#(mf/memo' % fill-menu-props-equals?)]} [{:keys [ids type values editor] :as props}] (let [locale (mf/deref i18n/locale) - show? (not (nil? (:fill-color values))) + show? (or (not (nil? (:fill-color values))) + (not (nil? (:fill-color-gradient values)))) label (case type :multiple (t locale "workspace.options.selection-fill") :group (t locale "workspace.options.group-fill") (t locale "workspace.options.fill")) - color {:value (:fill-color values) + color {:color (:fill-color values) :opacity (:fill-opacity values) :id (:fill-color-ref-id values) - :file-id (:fill-color-ref-file values)} + :file-id (:fill-color-ref-file values) + :gradient (:fill-color-gradient values)} on-add (mf/use-callback (mf/deps ids) (fn [event] - (st/emit! (dc/change-fill ids cp/default-color nil nil)))) + (st/emit! (dc/change-fill ids {:color cp/default-color + :opacity 1})))) on-delete (mf/use-callback (mf/deps ids) (fn [event] - (st/emit! (dc/change-fill ids nil nil nil)))) + (st/emit! (dc/change-fill ids nil)))) on-change (mf/use-callback (mf/deps ids) - (fn [value opacity id file-id] - (st/emit! (dc/change-fill ids value opacity id file-id)))) + (fn [color] + (st/emit! (dc/change-fill ids color)))) on-open-picker (mf/use-callback diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/frame_grid.cljs index 04e55265d..d1aba7a4f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/frame_grid.cljs @@ -101,14 +101,17 @@ (assoc-in [:params :item-length] item-length))))) handle-change-color - (fn [value opacity] - (emit-changes! #(-> % - (assoc-in [:params :color :value] value) - (assoc-in [:params :color :opacity] opacity)))) + (fn [color] + (emit-changes! #(-> % (assoc-in [:params :color] color)))) handle-use-default (fn [] - (emit-changes! #(hash-map :params ((:type grid) default-grid-params)))) + (let [params ((:type grid) default-grid-params) + color (or (get-in params [:color :value]) (get-in params [:color :color])) + params (-> params + (assoc-in [:color :color] color) + (update :color dissoc :value))] + (emit-changes! #(hash-map :params params)))) handle-set-as-default (fn [] @@ -214,6 +217,7 @@ :on-change (handle-change :params :margin)}]]) [:& color-row {:color (:color params) + :disable-gradient true :on-change handle-change-color}] [:div.row-flex [:button.btn-options {:disabled is-default diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index 04e6fe5e9..31381c3a0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -44,8 +44,9 @@ [:div.element-set [:div.element-set-title (t locale "workspace.options.canvas-background")] [:div.element-set-content - [:& color-row {:disable-opacity true - :color {:value (get options :background "#E8E9EA") + [:& color-row {:disable-gradient true + :disable-opacity true + :color {:color (get options :background "#E8E9EA") :opacity 1} :on-change handle-change-color :on-open on-open diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 021fb8bc1..2347e0d09 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -10,31 +10,33 @@ (ns app.main.ui.workspace.sidebar.options.rows.color-row (:require [rumext.alpha :as mf] + [cuerdas.core :as str] [app.common.math :as math] + [app.common.pages :as cp] + [app.common.data :as d] [app.util.dom :as dom] [app.util.data :refer [classnames]] [app.util.i18n :as i18n :refer [tr]] + [app.util.color :as uc] + [app.main.refs :as refs] [app.main.data.modal :as modal] - [app.common.data :as d] - [app.main.refs :as refs])) + [app.main.ui.components.color-bullet :as cb])) (defn color-picker-callback - [color handle-change-color handle-open handle-close disable-opacity] + [color disable-gradient disable-opacity handle-change-color handle-open handle-close] (fn [event] (let [x (.-clientX event) y (.-clientY event) props {:x x :y y + :disable-gradient disable-gradient + :disable-opacity disable-opacity :on-change handle-change-color :on-close handle-close - :value (:value color) - :opacity (:opacity color) - :disable-opacity disable-opacity}] + :data color}] (handle-open) (modal/show! :colorpicker props)))) -(defn value-to-background [value] - (if (= value :multiple) "transparent" value)) (defn remove-hash [value] (if (or (nil? value) (= value :multiple)) "" (subs value 1))) @@ -59,38 +61,28 @@ (if (= v :multiple) nil v)) (mf/defc color-row - [{:keys [color on-change on-open on-close disable-opacity]}] - (let [;; - file-colors (mf/deref refs/workspace-file-colors) + [{:keys [color disable-gradient disable-opacity on-change on-open on-close]}] + (let [file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) get-color-name (fn [{:keys [id file-id]}] (let [src-colors (if file-id (get-in shared-libs [file-id :data :colors]) file-colors)] (get-in src-colors [id :name]))) - default-color {:value "#000000" :opacity 1} - parse-color (fn [color] - (-> (merge default-color color) - (update :value #(or % "#000000")) - (update :opacity #(or % 1)))) - - state (mf/use-state (parse-color color)) - - value (:value @state) - opacity (:opacity @state) + (-> color + (update :color #(or % (:value color))))) change-value (fn [new-value] - (swap! state assoc :value new-value) - (when on-change (on-change new-value (remove-multiple opacity)))) + (when on-change (on-change (-> color + (assoc :color new-value) + (dissoc :gradient))))) change-opacity (fn [new-opacity] - (swap! state assoc :opacity new-opacity) - (when on-change (on-change (remove-multiple value) new-opacity))) + (when on-change (on-change (assoc color :opacity new-opacity)))) - handle-pick-color (fn [new-value new-opacity id file-id] - (reset! state {:value new-value :opacity new-opacity}) - (when on-change (on-change new-value new-opacity id file-id))) + handle-pick-color (fn [color] + (when on-change (on-change color))) handle-open (fn [] (when on-open (on-open))) @@ -114,37 +106,63 @@ change-opacity)))) select-all (fn [event] - (dom/select-text! (dom/get-target event)))] + (dom/select-text! (dom/get-target event))) + + handle-click-color (mf/use-callback + (mf/deps color) + (let [;; If multiple, we change to default color + color (if (uc/multiple? color) + {:color cp/default-color :opacity 1} + color)] + (color-picker-callback color + disable-gradient + disable-opacity + handle-pick-color + handle-open + handle-close)))] (mf/use-effect (mf/deps color) - #(reset! state (parse-color color))) + (fn [] + (modal/update-props! :colorpicker {:data (parse-color color)}))) [:div.row-flex.color-data - [:span.color-th - {:class (when (and (:id color) (not= (:id color) :multiple)) "color-name") - :style {:background-color (-> value value-to-background)} - :on-click (color-picker-callback @state handle-pick-color handle-open handle-close disable-opacity)} - (when (= value :multiple) "?")] + [:& cb/color-bullet {:color color + :on-click handle-click-color}] - (if (:id color) + (cond + ;; Rendering a color with ID + (:id color) [:div.color-info [:div.color-name (str (get-color-name color))]] + + ;; Rendering a gradient + (and (not (uc/multiple? color)) + (:gradient color) (get-in color [:gradient :type])) [:div.color-info - [:input {:value (-> value remove-hash) - :pattern "^[0-9a-fA-F]{0,6}$" - :placeholder (tr "settings.multiple") - :on-click select-all - :on-change handle-value-change}]]) + [:div.color-name (cb/gradient-type->string (get-in color [:gradient :type]))]] - (when (not disable-opacity) - [:div.input-element - {:class (classnames :percentail (not= opacity :multiple))} - [:input.input-text {:type "number" - :value (-> opacity opacity->string) - :placeholder (tr "settings.multiple") - :on-click select-all - :on-change handle-opacity-change - :min "0" - :max "100"}]])])) + ;; Rendering a plain color/opacity + :else + [:* + [:div.color-info + [:input {:value (if (uc/multiple? color) + "" + (-> color :color remove-hash)) + :pattern "^[0-9a-fA-F]{0,6}$" + :placeholder (tr "settings.multiple") + :on-click select-all + :on-change handle-value-change}]] + + (when (and (not disable-opacity) + (not (:gradient color))) + [:div.input-element + {:class (classnames :percentail (not= (:opacity color) :multiple))} + [:input.input-text {:type "number" + :value (-> color :opacity opacity->string) + :placeholder (tr "settings.multiple") + :on-click select-all + :on-change handle-opacity-change + :min "0" + :max "100"}]])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shadow.cljs index 0f13a906b..ceedd32f0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shadow.cljs @@ -174,7 +174,11 @@ [:span.after (t locale "workspace.options.shadow-options.spread")]]] [:div.color-row-wrap - [:& color-row {:color {:value (:color value) :opacity (:opacity value)} + [:& color-row {:color (if (string? (:color value)) + ;; Support for old format colors + {:color (:color value) :opacity (:opacity value)} + (:color value)) + :disable-gradient true :on-change (update-color index) :on-open #(st/emit! dwc/start-undo-transaction) :on-close #(st/emit! dwc/commit-undo-transaction)}]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs index a8927137b..4dd600c14 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs @@ -14,6 +14,7 @@ [app.common.data :as d] [app.common.math :as math] [app.main.data.workspace.common :as dwc] + [app.main.data.colors :as dc] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] @@ -29,7 +30,8 @@ :stroke-color :stroke-color-ref-id :stroke-color-ref-file - :stroke-opacity]) + :stroke-opacity + :stroke-color-gradient]) (defn- stroke-menu-props-equals? [np op] @@ -72,19 +74,17 @@ show-options (not= (:stroke-style values :none) :none) - current-stroke-color {:value (:stroke-color values) + current-stroke-color {:color (:stroke-color values) :opacity (:stroke-opacity values) :id (:stroke-color-ref-id values) - :file-id (:stroke-color-ref-file values)} + :file-id (:stroke-color-ref-file values) + :gradient (:stroke-color-gradient values)} handle-change-stroke-color - (fn [value opacity id file-id] - (let [change #(cond-> % - value (assoc :stroke-color value - :stroke-color-ref-id id - :stroke-color-ref-file file-id) - opacity (assoc :stroke-opacity opacity))] - (st/emit! (dwc/update-shapes ids change)))) + (mf/use-callback + (mf/deps ids) + (fn [color] + (st/emit! (dc/change-stroke ids color)))) on-stroke-style-change (fn [event] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/text.cljs index 0c7d17768..160c19322 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/text.cljs @@ -32,7 +32,7 @@ ["slate" :refer [Transforms]])) (def text-typography-attrs [:typography-ref-id :typography-ref-file]) -(def text-fill-attrs [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill :opacity ]) +(def text-fill-attrs [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient :fill :opacity ]) (def text-font-attrs [:font-id :font-family :font-variant-id :font-size :font-weight :font-style]) (def text-align-attrs [:text-align]) (def text-spacing-attrs [:line-height :letter-spacing]) @@ -291,6 +291,8 @@ :shape shape :attrs text-fill-attrs}) + fill-values (d/update-in-when fill-values [:fill-color-gradient :type] keyword) + fill-values (cond-> fill-values ;; Keep for backwards compatibility (:fill fill-values) (assoc :fill-color (:fill fill-values)) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index ce0ef64f9..256b577b5 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -40,6 +40,8 @@ [app.main.ui.workspace.snap-distances :refer [snap-distances]] [app.main.ui.workspace.frame-grid :refer [frame-grid]] [app.main.ui.workspace.shapes.outline :refer [outline]] + [app.main.ui.workspace.gradients :refer [gradient-handlers]] + [app.main.ui.workspace.colorpicker.pixel-overlay :refer [pixel-overlay]] [app.common.math :as mth] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] @@ -184,104 +186,6 @@ (:width vbox 0) (:height vbox 0)])) -(mf/defc pixel-picker-overlay - {::mf/wrap-props false} - [props] - (let [vport (unchecked-get props "vport") - vbox (unchecked-get props "vbox") - viewport-ref (unchecked-get props "viewport-ref") - options (unchecked-get props "options") - svg-ref (mf/use-ref nil) - canvas-ref (mf/use-ref nil) - fetch-pending (mf/deref (mdf/pending-ref)) - - on-mouse-move-picker - (fn [event] - (when-let [zoom-view-node (.getElementById js/document "picker-detail")] - (let [{brx :left bry :top} (dom/get-bounding-rect (mf/ref-val viewport-ref)) - x (- (.-clientX event) brx) - y (- (.-clientY event) bry) - - zoom-context (.getContext zoom-view-node "2d") - canvas-node (mf/ref-val canvas-ref) - canvas-context (.getContext canvas-node "2d") - pixel-data (.getImageData canvas-context x y 1 1) - rgba (.-data pixel-data) - r (obj/get rgba 0) - g (obj/get rgba 1) - b (obj/get rgba 2) - a (obj/get rgba 3) - - area-data (.getImageData canvas-context (- x 25) (- y 20) 50 40)] - - (-> (js/createImageBitmap area-data) - (p/then (fn [image] - ;; Draw area - (obj/set! zoom-context "imageSmoothingEnabled" false) - (.drawImage zoom-context image 0 0 200 160)))) - (st/emit! (dwc/pick-color [r g b a]))))) - - on-mouse-down-picker - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (st/emit! (dwc/pick-color-select true (kbd/shift? event)))) - - on-mouse-up-picker - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (st/emit! (dwc/stop-picker)) - (modal/disallow-click-outside!))] - - (mf/use-effect - ;; Everytime we finish retrieving a new URL we redraw the canvas - ;; so even if we're not finished the user can start to pick basic - ;; shapes - (mf/deps fetch-pending) - (fn [] - (try - (let [canvas-node (mf/ref-val canvas-ref) - canvas-context (.getContext canvas-node "2d") - svg-node (mf/ref-val svg-ref)] - (timers/schedule 100 - #(let [xml (.serializeToString (js/XMLSerializer.) svg-node) - img-src (str "data:image/svg+xml;base64," - (-> xml js/encodeURIComponent js/unescape js/btoa)) - img (js/Image.) - on-error (fn [err] (.error js/console "ERROR" err)) - on-load (fn [] (.drawImage canvas-context img 0 0))] - (.addEventListener img "error" on-error) - (.addEventListener img "load" on-load) - (obj/set! img "src" img-src)))) - (catch :default e (.error js/console e))))) - - [:* - [:div.overlay - {:style {:position "absolute" - :top 0 - :left 0 - :width "100%" - :height "100%" - :cursor cur/picker} - :on-mouse-down on-mouse-down-picker - :on-mouse-up on-mouse-up-picker - :on-mouse-move on-mouse-move-picker}] - [:canvas {:ref canvas-ref - :width (:width vport 0) - :height (:height vport 0) - :style {:display "none"}}] - [:& (mf/provider muc/embed-ctx) {:value true} - [:svg.viewport - {:ref svg-ref - :preserveAspectRatio "xMidYMid meet" - :width (:width vport 0) - :height (:height vport 0) - :view-box (format-viewbox vbox) - :style {:display "none" - :background-color (get options :background "#E8E9EA")}} - [:& frames]]]])) - (mf/defc viewport [{:keys [page-id page local layout] :as props}] (let [{:keys [options-mode @@ -309,8 +213,6 @@ drawing-tool (:tool drawing) drawing-obj (:object drawing) - pick-color (mf/use-state [255 255 255 255]) - zoom (or zoom 1) on-mouse-down @@ -586,11 +488,11 @@ [:* (when picking-color? - [:& pixel-picker-overlay {:vport vport - :vbox vbox - :viewport-ref viewport-ref - :options options - :layout layout}]) + [:& pixel-overlay {:vport vport + :vbox vbox + :viewport-ref viewport-ref + :options options + :layout layout}]) [:svg.viewport {:preserveAspectRatio "xMidYMid meet" @@ -642,6 +544,10 @@ :zoom zoom :edition edition}]) + (when (= (count selected) 1) + [:& gradient-handlers {:id (first selected) + :zoom zoom}]) + (when drawing-obj [:& draw-area {:shape drawing-obj :zoom zoom diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index d80f4436c..2ed47a672 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -77,3 +77,35 @@ (defn hsv->hsl [hsv] (hex->hsl (hsv->hex hsv))) + +(defn gradient->css [{:keys [type stops]}] + (let [parse-stop + (fn [{:keys [offset color opacity]}] + (let [[r g b] (hex->rgb color)] + (str/fmt "rgba(%s, %s, %s, %s) %s" r g b opacity (str (* offset 100) "%")))) + + stops-css (str/join "," (map parse-stop stops))] + + (if (= type :linear) + (str/fmt "linear-gradient(to bottom, %s)" stops-css) + (str/fmt "radial-gradient(circle, %s)" stops-css)))) + +;; TODO: REMOVE `VALUE` WHEN COLOR IS INTEGRATED +(defn color->background [{:keys [color opacity gradient value]}] + (let [color (or color value) + opacity (or opacity 1)] + (cond + (and gradient (not= :multiple gradient)) + (gradient->css gradient) + + (not= color :multiple) + (let [[r g b] (hex->rgb (or color value))] + (str/fmt "rgba(%s, %s, %s, %s)" r g b opacity)) + + :else "transparent"))) + +(defn multiple? [{:keys [value color gradient]}] + (or (= value :multiple) + (= color :multiple) + (= gradient :multiple) + (and gradient color))) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index def453015..a867cc646 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -15,6 +15,8 @@ [goog.object :as gobj] ["lodash/omit" :as omit])) +(defn new [] #js {}) + (defn get ([obj k] (when-not (nil? obj)