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)