Merge pull request #358 from uxbox/560/gradients

560/gradients
This commit is contained in:
Andrey Antukh 2020-10-16 11:47:11 +02:00 committed by GitHub
commit 567e177699
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 2310 additions and 1377 deletions

View file

@ -55,6 +55,36 @@
(>= % min-safe-int) (>= % min-safe-int)
(<= % max-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 ;; Page Options
(s/def :internal.page.grid.color/value string?) (s/def :internal.page.grid.color/value string?)
(s/def :internal.page.grid.color/opacity ::safe-number) (s/def :internal.page.grid.color/opacity ::safe-number)
@ -110,10 +140,13 @@
(s/def :internal.shape/blocked boolean?) (s/def :internal.shape/blocked boolean?)
(s/def :internal.shape/collapsed boolean?) (s/def :internal.shape/collapsed boolean?)
(s/def :internal.shape/content any?) (s/def :internal.shape/content any?)
(s/def :internal.shape/fill-color string?) (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-file (s/nilable uuid?))
(s/def :internal.shape/fill-color-ref-id (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-family string?)
(s/def :internal.shape/font-size ::safe-integer) (s/def :internal.shape/font-size ::safe-integer)
(s/def :internal.shape/font-style string?) (s/def :internal.shape/font-style string?)
@ -262,13 +295,26 @@
:internal.page/options :internal.page/options
:internal.page/objects])) :internal.page/objects]))
(s/def :internal.color/name ::string) (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/def ::color
(s/keys :req-un [::id (s/keys :req-un [::id
:internal.color/name :internal.color/name]
:internal.color/value])) :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/name ::string)
(s/def :internal.media-object/path ::string) (s/def :internal.media-object/path ::string)
@ -294,7 +340,7 @@
(s/map-of ::uuid ::color)) (s/map-of ::uuid ::color))
(s/def :internal.file/recent-colors (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/id ::id)
(s/def :internal.typography/name ::string) (s/def :internal.typography/name ::string)
@ -408,8 +454,10 @@
(defmethod change-spec :del-color [_] (defmethod change-spec :del-color [_]
(s/keys :req-un [::id])) (s/keys :req-un [::id]))
(s/def :internal.changes.add-recent-color/color ::recent-color)
(defmethod change-spec :add-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) (s/def :internal.changes.media/object ::media-object)
@ -821,7 +869,7 @@
(defmethod process-change :mod-color (defmethod process-change :mod-color
[data {:keys [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 (defmethod process-change :del-color
[data {:keys [id]}] [data {:keys [id]}]
@ -917,3 +965,4 @@
(ex/raise :type :not-implemented (ex/raise :type :not-implemented
:code :operation-not-implemented :code :operation-not-implemented
:context {:type (:type op)})) :context {:type (:type op)}))

View file

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" height="500" width="500"><path d="M24.7-.203C11.421-.203.001 11.217.001 24.494v451.012c0 13.277 11.417 24.695 24.691 24.697h450.608c13.28 0 24.697-11.42 24.697-24.697V24.494c0-13.276-11.42-24.697-24.697-24.697H24.699zm4.142 31.35h442.316v437.707H28.842V31.146z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M24.7-.203C11.421-.203.001 11.217.001 24.494v451.012c0 13.277 11.417 24.695 24.691 24.697H475.3c13.28 0 24.697-11.42 24.697-24.697V24.494c0-13.276-11.42-24.697-24.697-24.697H24.699zm12.142 39.35h426.316v421.707H36.842V39.146z"/>
</svg>

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 310 B

Before After
Before After

View file

@ -1,4 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
<path fill="#fff" d="M.607 13.076v2.401l2.224.025 7.86-7.405s-.05-.885-.253-1.087c-.202-.202-2.25-1.744-2.25-1.744z"/>
<path fill="#000" d="M.343 15.974a.514.514 0 01-.317-.321c-.023-.07-.026-.23-.026-1.43 0-1.468-.001-1.445.09-1.586.02-.032 1.703-1.724 3.74-3.759a596.805 596.805 0 003.7-3.716c0-.009-.367-.384-.816-.833a29.9 29.9 0 01-.817-.833c0-.01.474-.49 1.054-1.07l1.053-1.053.948.946.947.947 1.417-1.413C12.366.806 12.765.418 12.856.357c.238-.161.52-.28.792-.334.17-.034.586-.03.76.008.801.173 1.41.794 1.57 1.603.03.15.03.569 0 .718a2.227 2.227 0 01-.334.793c-.061.09-.45.49-1.496 1.54L12.734 6.1l.947.948.947.947-1.053 1.054c-.58.58-1.061 1.054-1.07 1.054-.01 0-.384-.368-.833-.817-.45-.45-.824-.817-.834-.817-.009 0-1.68 1.666-3.716 3.701a493.093 493.093 0 01-3.759 3.74c-.14.091-.117.09-1.59.089-1.187 0-1.366-.004-1.43-.027zm6.024-4.633a592.723 592.723 0 003.663-3.68c0-.02-1.67-1.69-1.69-1.69-.01 0-1.666 1.648-3.68 3.663L.996 13.297v.834c0 .627.005.839.02.854.015.014.227.02.854.02h.833l3.664-3.664z"/> <path fill="#000" d="M.343 15.974a.514.514 0 01-.317-.321c-.023-.07-.026-.23-.026-1.43 0-1.468-.001-1.445.09-1.586.02-.032 1.703-1.724 3.74-3.759a596.805 596.805 0 003.7-3.716c0-.009-.367-.384-.816-.833a29.9 29.9 0 01-.817-.833c0-.01.474-.49 1.054-1.07l1.053-1.053.948.946.947.947 1.417-1.413C12.366.806 12.765.418 12.856.357c.238-.161.52-.28.792-.334.17-.034.586-.03.76.008.801.173 1.41.794 1.57 1.603.03.15.03.569 0 .718a2.227 2.227 0 01-.334.793c-.061.09-.45.49-1.496 1.54L12.734 6.1l.947.948.947.947-1.053 1.054c-.58.58-1.061 1.054-1.07 1.054-.01 0-.384-.368-.833-.817-.45-.45-.824-.817-.834-.817-.009 0-1.68 1.666-3.716 3.701a493.093 493.093 0 01-3.759 3.74c-.14.091-.117.09-1.59.089-1.187 0-1.366-.004-1.43-.027zm6.024-4.633a592.723 592.723 0 003.663-3.68c0-.02-1.67-1.69-1.69-1.69-.01 0-1.666 1.648-3.68 3.663L.996 13.297v.834c0 .627.005.839.02.854.015.014.227.02.854.02h.833l3.664-3.664z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 981 B

Before After
Before After

View file

@ -18,7 +18,7 @@
} }
}, },
"auth.create-demo-profile" : { "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" : { "translations" : {
"en" : "Just wanna try it?", "en" : "Just wanna try it?",
"fr" : "Vous voulez juste essayer?", "fr" : "Vous voulez juste essayer?",
@ -36,7 +36,7 @@
} }
}, },
"auth.email" : { "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" : { "translations" : {
"en" : "Email", "en" : "Email",
"fr" : "Adresse email", "fr" : "Adresse email",
@ -177,7 +177,7 @@
} }
}, },
"auth.password" : { "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" : { "translations" : {
"en" : "Password", "en" : "Password",
"fr" : "Mot de passe", "fr" : "Mot de passe",
@ -276,7 +276,7 @@
} }
}, },
"dashboard.add-shared" : { "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" : { "translations" : {
"en" : "Add as Shared Library", "en" : "Add as Shared Library",
"fr" : "", "fr" : "",
@ -322,7 +322,7 @@
} }
}, },
"dashboard.empty-files" : { "dashboard.empty-files" : {
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:184" ], "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:187" ],
"translations" : { "translations" : {
"en" : "You still have no files here", "en" : "You still have no files here",
"fr" : "Vous n'avez encore aucun fichier ici", "fr" : "Vous n'avez encore aucun fichier ici",
@ -533,7 +533,7 @@
} }
}, },
"dashboard.remove-shared" : { "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" : { "translations" : {
"en" : "Remove as Shared Library", "en" : "Remove as Shared Library",
"fr" : "", "fr" : "",
@ -578,7 +578,7 @@
} }
}, },
"dashboard.show-all-files" : { "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" : { "translations" : {
"en" : "Show all files", "en" : "Show all files",
"es" : "Ver todos los ficheros" "es" : "Ver todos los ficheros"
@ -636,7 +636,7 @@
} }
}, },
"dashboard.update-settings" : { "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" : { "translations" : {
"en" : "Update settings", "en" : "Update settings",
"fr" : "Mettre à jour les paramètres", "fr" : "Mettre à jour les paramètres",
@ -645,7 +645,7 @@
} }
}, },
"dashboard.your-account-title" : { "dashboard.your-account-title" : {
"used-in" : [ "src/app/main/ui/settings.cljs:28" ], "used-in" : [ "src/app/main/ui/settings.cljs:29" ],
"translations" : { "translations" : {
"en" : "Your account", "en" : "Your account",
"es" : "Su cuenta" "es" : "Su cuenta"
@ -712,7 +712,7 @@
"unused" : true "unused" : true
}, },
"ds.confirm-cancel" : { "ds.confirm-cancel" : {
"used-in" : [ "src/app/main/ui/confirm.cljs:28" ], "used-in" : [ "src/app/main/ui/confirm.cljs:36" ],
"translations" : { "translations" : {
"en" : "Cancel", "en" : "Cancel",
"fr" : "Annuler", "fr" : "Annuler",
@ -721,7 +721,7 @@
} }
}, },
"ds.confirm-ok" : { "ds.confirm-ok" : {
"used-in" : [ "src/app/main/ui/confirm.cljs:29" ], "used-in" : [ "src/app/main/ui/confirm.cljs:37" ],
"translations" : { "translations" : {
"en" : "Ok", "en" : "Ok",
"fr" : "Ok", "fr" : "Ok",
@ -730,7 +730,7 @@
} }
}, },
"ds.confirm-title" : { "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" : { "translations" : {
"en" : "Are you sure?", "en" : "Are you sure?",
"fr" : "Êtes-vous sûr?", "fr" : "Êtes-vous sûr?",
@ -757,7 +757,7 @@
} }
}, },
"errors.email-already-exists" : { "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" : { "translations" : {
"en" : "Email already used", "en" : "Email already used",
"fr" : "Adresse e-mail déjà utilisée", "fr" : "Adresse e-mail déjà utilisée",
@ -784,7 +784,7 @@
} }
}, },
"errors.generic" : { "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" : { "translations" : {
"en" : "Something wrong has happened.", "en" : "Something wrong has happened.",
"fr" : "Quelque chose c'est mal passé.", "fr" : "Quelque chose c'est mal passé.",
@ -811,7 +811,7 @@
} }
}, },
"errors.media-type-mismatch" : { "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" : { "translations" : {
"en" : "Seems that the contents of the image does not match the file extension.", "en" : "Seems that the contents of the image does not match the file extension.",
"fr" : "", "fr" : "",
@ -820,7 +820,7 @@
} }
}, },
"errors.media-type-not-allowed" : { "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" : { "translations" : {
"en" : "Seems that this is not a valid image.", "en" : "Seems that this is not a valid image.",
"fr" : "", "fr" : "",
@ -865,7 +865,7 @@
} }
}, },
"errors.unexpected-error" : { "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" : { "translations" : {
"en" : "An unexpected error occurred.", "en" : "An unexpected error occurred.",
"fr" : "Une erreur inattendue c'est produite", "fr" : "Une erreur inattendue c'est produite",
@ -931,7 +931,7 @@
} }
}, },
"labels.delete" : { "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" : { "translations" : {
"en" : "Delete", "en" : "Delete",
"fr" : "Supprimer", "fr" : "Supprimer",
@ -973,7 +973,7 @@
} }
}, },
"labels.logout" : { "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" : { "translations" : {
"en" : "Logout", "en" : "Logout",
"fr" : "Quitter", "fr" : "Quitter",
@ -982,7 +982,7 @@
} }
}, },
"labels.members" : { "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" : { "translations" : {
"en" : "Members" "en" : "Members"
} }
@ -1064,7 +1064,7 @@
} }
}, },
"labels.rename" : { "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" : { "translations" : {
"en" : "Rename", "en" : "Rename",
"es" : "Renombrar" "es" : "Renombrar"
@ -1077,7 +1077,7 @@
} }
}, },
"labels.settings" : { "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" : { "translations" : {
"en" : "Settings", "en" : "Settings",
"fr" : "Settings", "fr" : "Settings",
@ -1111,7 +1111,7 @@
} }
}, },
"media.loading" : { "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" : { "translations" : {
"en" : "Loading image...", "en" : "Loading image...",
"fr" : "Chargement de l'image...", "fr" : "Chargement de l'image...",
@ -1129,7 +1129,7 @@
"unused" : true "unused" : true
}, },
"modals.add-shared-confirm.accept" : { "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" : { "translations" : {
"en" : "Add as Shared Library", "en" : "Add as Shared Library",
"fr" : "", "fr" : "",
@ -1147,7 +1147,7 @@
} }
}, },
"modals.add-shared-confirm.message" : { "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" : { "translations" : {
"en" : "Add “%s” as Shared Library", "en" : "Add “%s” as Shared Library",
"fr" : "", "fr" : "",
@ -1381,7 +1381,7 @@
} }
}, },
"modals.remove-shared-confirm.accept" : { "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" : { "translations" : {
"en" : "Remove as Shared Library", "en" : "Remove as Shared Library",
"fr" : "", "fr" : "",
@ -1390,7 +1390,7 @@
} }
}, },
"modals.remove-shared-confirm.hint" : { "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" : { "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.", "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" : "", "fr" : "",
@ -1399,7 +1399,7 @@
} }
}, },
"modals.remove-shared-confirm.message" : { "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" : { "translations" : {
"en" : "Remove “%s” as Shared Library", "en" : "Remove “%s” as Shared Library",
"fr" : "", "fr" : "",
@ -1417,7 +1417,7 @@
} }
}, },
"notifications.profile-saved" : { "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" : { "translations" : {
"en" : "Profile saved successfully!", "en" : "Profile saved successfully!",
"fr" : "Profil enregistré avec succès!", "fr" : "Profil enregistré avec succès!",
@ -1426,7 +1426,7 @@
} }
}, },
"notifications.validation-email-sent" : { "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" : { "translations" : {
"en" : "Verification email sent to %s; check your email!" "en" : "Verification email sent to %s; check your email!"
} }
@ -1441,7 +1441,7 @@
} }
}, },
"settings.multiple" : { "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" : { "translations" : {
"en" : "Mixed", "en" : "Mixed",
"fr" : null, "fr" : null,
@ -1666,7 +1666,7 @@
} }
}, },
"workspace.assets.assets" : { "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" : { "translations" : {
"en" : "Assets", "en" : "Assets",
"fr" : "", "fr" : "",
@ -1675,7 +1675,7 @@
} }
}, },
"workspace.assets.box-filter-all" : { "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" : { "translations" : {
"en" : "All assets", "en" : "All assets",
"fr" : "", "fr" : "",
@ -1702,7 +1702,7 @@
"unused" : true "unused" : true
}, },
"workspace.assets.colors" : { "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" : { "translations" : {
"en" : "Colors", "en" : "Colors",
"fr" : "", "fr" : "",
@ -1711,7 +1711,7 @@
} }
}, },
"workspace.assets.components" : { "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" : { "translations" : {
"en" : "Components", "en" : "Components",
"fr" : "", "fr" : "",
@ -1720,7 +1720,7 @@
} }
}, },
"workspace.assets.delete" : { "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" : { "translations" : {
"en" : "Delete", "en" : "Delete",
"fr" : "", "fr" : "",
@ -1729,7 +1729,7 @@
} }
}, },
"workspace.assets.edit" : { "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" : { "translations" : {
"en" : "Edit", "en" : "Edit",
"fr" : "", "fr" : "",
@ -1738,7 +1738,7 @@
} }
}, },
"workspace.assets.file-library" : { "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" : { "translations" : {
"en" : "File library", "en" : "File library",
"fr" : "", "fr" : "",
@ -1747,7 +1747,7 @@
} }
}, },
"workspace.assets.graphics" : { "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" : { "translations" : {
"en" : "Graphics", "en" : "Graphics",
"fr" : "", "fr" : "",
@ -1756,7 +1756,7 @@
} }
}, },
"workspace.assets.libraries" : { "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" : { "translations" : {
"en" : "Libraries", "en" : "Libraries",
"fr" : "", "fr" : "",
@ -1765,7 +1765,7 @@
} }
}, },
"workspace.assets.not-found" : { "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" : { "translations" : {
"en" : "No assets found", "en" : "No assets found",
"fr" : "", "fr" : "",
@ -1774,7 +1774,7 @@
} }
}, },
"workspace.assets.rename" : { "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" : { "translations" : {
"en" : "Rename", "en" : "Rename",
"fr" : "", "fr" : "",
@ -1783,7 +1783,7 @@
} }
}, },
"workspace.assets.search" : { "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" : { "translations" : {
"en" : "Search assets", "en" : "Search assets",
"fr" : "", "fr" : "",
@ -1792,7 +1792,7 @@
} }
}, },
"workspace.assets.shared" : { "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" : { "translations" : {
"en" : "SHARED", "en" : "SHARED",
"fr" : "", "fr" : "",
@ -1801,7 +1801,7 @@
} }
}, },
"workspace.assets.typography" : { "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" : { "translations" : {
"en" : "Typographies" "en" : "Typographies"
} }
@ -1854,8 +1854,20 @@
"en" : "Text Transform" "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" : { "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" : { "translations" : {
"en" : "Disable dynamic alignment", "en" : "Disable dynamic alignment",
"fr" : "Désactiver l'alignement dynamique", "fr" : "Désactiver l'alignement dynamique",
@ -1864,7 +1876,7 @@
} }
}, },
"workspace.header.menu.disable-snap-grid" : { "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" : { "translations" : {
"en" : "Disable snap to grid", "en" : "Disable snap to grid",
"fr" : "Désactiver l'alignement sur la grille", "fr" : "Désactiver l'alignement sur la grille",
@ -1873,7 +1885,7 @@
} }
}, },
"workspace.header.menu.enable-dynamic-alignment" : { "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" : { "translations" : {
"en" : "Enable dynamic aligment", "en" : "Enable dynamic aligment",
"fr" : "Activer l'alignement dynamique", "fr" : "Activer l'alignement dynamique",
@ -1882,7 +1894,7 @@
} }
}, },
"workspace.header.menu.enable-snap-grid" : { "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" : { "translations" : {
"en" : "Snap to grid", "en" : "Snap to grid",
"fr" : "Aligner sur la grille", "fr" : "Aligner sur la grille",
@ -1891,7 +1903,7 @@
} }
}, },
"workspace.header.menu.hide-assets" : { "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" : { "translations" : {
"en" : "Hide assets", "en" : "Hide assets",
"fr" : "", "fr" : "",
@ -1900,7 +1912,7 @@
} }
}, },
"workspace.header.menu.hide-grid" : { "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" : { "translations" : {
"en" : "Hide grids", "en" : "Hide grids",
"fr" : "Masquer la grille", "fr" : "Masquer la grille",
@ -1909,7 +1921,7 @@
} }
}, },
"workspace.header.menu.hide-layers" : { "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" : { "translations" : {
"en" : "Hide layers", "en" : "Hide layers",
"fr" : "Masquer les couches", "fr" : "Masquer les couches",
@ -1918,7 +1930,7 @@
} }
}, },
"workspace.header.menu.hide-palette" : { "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" : { "translations" : {
"en" : "Hide color palette", "en" : "Hide color palette",
"fr" : "Masquer la palette de couleurs", "fr" : "Masquer la palette de couleurs",
@ -1927,7 +1939,7 @@
} }
}, },
"workspace.header.menu.hide-rules" : { "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" : { "translations" : {
"en" : "Hide rules", "en" : "Hide rules",
"fr" : "Masquer les règles", "fr" : "Masquer les règles",
@ -1936,7 +1948,7 @@
} }
}, },
"workspace.header.menu.show-assets" : { "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" : { "translations" : {
"en" : "Show assets", "en" : "Show assets",
"fr" : "", "fr" : "",
@ -1945,7 +1957,7 @@
} }
}, },
"workspace.header.menu.show-grid" : { "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" : { "translations" : {
"en" : "Show grid", "en" : "Show grid",
"fr" : "Montrer la grille", "fr" : "Montrer la grille",
@ -1954,7 +1966,7 @@
} }
}, },
"workspace.header.menu.show-layers" : { "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" : { "translations" : {
"en" : "Show layers", "en" : "Show layers",
"fr" : "Montrer les couches", "fr" : "Montrer les couches",
@ -1963,7 +1975,7 @@
} }
}, },
"workspace.header.menu.show-palette" : { "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" : { "translations" : {
"en" : "Show color palette", "en" : "Show color palette",
"fr" : "Montrer la palette de couleurs", "fr" : "Montrer la palette de couleurs",
@ -1972,7 +1984,7 @@
} }
}, },
"workspace.header.menu.show-rules" : { "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" : { "translations" : {
"en" : "Show rules", "en" : "Show rules",
"fr" : "Montrer les règles", "fr" : "Montrer les règles",
@ -2005,7 +2017,7 @@
} }
}, },
"workspace.header.viewer" : { "workspace.header.viewer" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:260" ], "used-in" : [ "src/app/main/ui/workspace/header.cljs:263" ],
"translations" : { "translations" : {
"en" : "View mode (Ctrl + P)", "en" : "View mode (Ctrl + P)",
"fr" : "Mode visualisation (Ctrl + P)", "fr" : "Mode visualisation (Ctrl + P)",
@ -2038,19 +2050,19 @@
} }
}, },
"workspace.libraries.colors.file-library" : { "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" : { "translations" : {
"en" : "File library" "en" : "File library"
} }
}, },
"workspace.libraries.colors.recent-colors" : { "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" : { "translations" : {
"en" : "Recent colors" "en" : "Recent colors"
} }
}, },
"workspace.libraries.colors.save-color" : { "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" : { "translations" : {
"en" : "Save color" "en" : "Save color"
} }
@ -2290,7 +2302,7 @@
} }
}, },
"workspace.options.fill" : { "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" : { "translations" : {
"en" : "Fill", "en" : "Fill",
"fr" : "Remplissage", "fr" : "Remplissage",
@ -2308,7 +2320,7 @@
} }
}, },
"workspace.options.grid.column" : { "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" : { "translations" : {
"en" : "Columns", "en" : "Columns",
"fr" : "Colonnes", "fr" : "Colonnes",
@ -2317,7 +2329,7 @@
} }
}, },
"workspace.options.grid.params.columns" : { "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" : { "translations" : {
"en" : "Columns", "en" : "Columns",
"fr" : "Colonnes", "fr" : "Colonnes",
@ -2326,7 +2338,7 @@
} }
}, },
"workspace.options.grid.params.gutter" : { "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" : { "translations" : {
"en" : "Gutter", "en" : "Gutter",
"fr" : "Gouttière", "fr" : "Gouttière",
@ -2335,7 +2347,7 @@
} }
}, },
"workspace.options.grid.params.height" : { "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" : { "translations" : {
"en" : "Height", "en" : "Height",
"fr" : "Hauteur", "fr" : "Hauteur",
@ -2344,7 +2356,7 @@
} }
}, },
"workspace.options.grid.params.margin" : { "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" : { "translations" : {
"en" : "Margin", "en" : "Margin",
"fr" : "Marge", "fr" : "Marge",
@ -2353,7 +2365,7 @@
} }
}, },
"workspace.options.grid.params.rows" : { "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" : { "translations" : {
"en" : "Rows", "en" : "Rows",
"fr" : "Lignes", "fr" : "Lignes",
@ -2362,7 +2374,7 @@
} }
}, },
"workspace.options.grid.params.set-default" : { "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" : { "translations" : {
"en" : "Set as default", "en" : "Set as default",
"fr" : "Définir par défaut", "fr" : "Définir par défaut",
@ -2371,7 +2383,7 @@
} }
}, },
"workspace.options.grid.params.size" : { "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" : { "translations" : {
"en" : "Size", "en" : "Size",
"fr" : "Taille", "fr" : "Taille",
@ -2380,7 +2392,7 @@
} }
}, },
"workspace.options.grid.params.type" : { "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" : { "translations" : {
"en" : "Type", "en" : "Type",
"fr" : "Type", "fr" : "Type",
@ -2389,7 +2401,7 @@
} }
}, },
"workspace.options.grid.params.type.bottom" : { "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" : { "translations" : {
"en" : "Bottom", "en" : "Bottom",
"fr" : "Bas", "fr" : "Bas",
@ -2398,7 +2410,7 @@
} }
}, },
"workspace.options.grid.params.type.center" : { "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" : { "translations" : {
"en" : "Center", "en" : "Center",
"fr" : "Centre", "fr" : "Centre",
@ -2407,7 +2419,7 @@
} }
}, },
"workspace.options.grid.params.type.left" : { "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" : { "translations" : {
"en" : "Left", "en" : "Left",
"fr" : "Gauche", "fr" : "Gauche",
@ -2416,7 +2428,7 @@
} }
}, },
"workspace.options.grid.params.type.right" : { "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" : { "translations" : {
"en" : "Right", "en" : "Right",
"fr" : "Droite", "fr" : "Droite",
@ -2425,7 +2437,7 @@
} }
}, },
"workspace.options.grid.params.type.stretch" : { "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" : { "translations" : {
"en" : "Stretch", "en" : "Stretch",
"fr" : "Étirer", "fr" : "Étirer",
@ -2434,7 +2446,7 @@
} }
}, },
"workspace.options.grid.params.type.top" : { "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" : { "translations" : {
"en" : "Top", "en" : "Top",
"fr" : "Haut", "fr" : "Haut",
@ -2443,7 +2455,7 @@
} }
}, },
"workspace.options.grid.params.use-default" : { "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" : { "translations" : {
"en" : "Use default", "en" : "Use default",
"fr" : "Utiliser la valeur par défaut", "fr" : "Utiliser la valeur par défaut",
@ -2452,7 +2464,7 @@
} }
}, },
"workspace.options.grid.params.width" : { "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" : { "translations" : {
"en" : "Width", "en" : "Width",
"fr" : "Largeur", "fr" : "Largeur",
@ -2461,7 +2473,7 @@
} }
}, },
"workspace.options.grid.row" : { "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" : { "translations" : {
"en" : "Rows", "en" : "Rows",
"fr" : "Lignes", "fr" : "Lignes",
@ -2470,7 +2482,7 @@
} }
}, },
"workspace.options.grid.square" : { "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" : { "translations" : {
"en" : "Square", "en" : "Square",
"fr" : "Carré", "fr" : "Carré",
@ -2479,7 +2491,7 @@
} }
}, },
"workspace.options.grid.title" : { "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" : { "translations" : {
"en" : "Grid & Layouts", "en" : "Grid & Layouts",
"fr" : "Grille & couches", "fr" : "Grille & couches",
@ -2488,7 +2500,7 @@
} }
}, },
"workspace.options.group-fill" : { "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" : { "translations" : {
"en" : "Group fill", "en" : "Group fill",
"fr" : null, "fr" : null,
@ -2497,7 +2509,7 @@
} }
}, },
"workspace.options.group-stroke" : { "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" : { "translations" : {
"en" : "Group stroke", "en" : "Group stroke",
"fr" : null, "fr" : null,
@ -2524,7 +2536,7 @@
} }
}, },
"workspace.options.position" : { "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" : { "translations" : {
"en" : "Position", "en" : "Position",
"fr" : "Position", "fr" : "Position",
@ -2578,7 +2590,7 @@
} }
}, },
"workspace.options.selection-fill" : { "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" : { "translations" : {
"en" : "Selection fill", "en" : "Selection fill",
"fr" : null, "fr" : null,
@ -2587,7 +2599,7 @@
} }
}, },
"workspace.options.selection-stroke" : { "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" : { "translations" : {
"en" : "Selection stroke", "en" : "Selection stroke",
"fr" : null, "fr" : null,
@ -2632,13 +2644,13 @@
} }
}, },
"workspace.options.shadow-options.title" : { "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" : { "translations" : {
"en" : "Shadow" "en" : "Shadow"
} }
}, },
"workspace.options.size" : { "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" : { "translations" : {
"en" : "Size", "en" : "Size",
"fr" : "Taille", "fr" : "Taille",
@ -2656,7 +2668,7 @@
} }
}, },
"workspace.options.stroke" : { "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" : { "translations" : {
"en" : "Stroke", "en" : "Stroke",
"fr" : "Bordure", "fr" : "Bordure",
@ -2845,7 +2857,7 @@
} }
}, },
"workspace.options.text-options.none" : { "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" : { "translations" : {
"en" : "None", "en" : "None",
"fr" : "Aucune", "fr" : "Aucune",
@ -2960,7 +2972,7 @@
} }
}, },
"workspace.sitemap" : { "workspace.sitemap" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:146" ], "used-in" : [ "src/app/main/ui/workspace/header.cljs:149" ],
"translations" : { "translations" : {
"en" : "Sitemap", "en" : "Sitemap",
"fr" : null, "fr" : null,
@ -3059,7 +3071,7 @@
} }
}, },
"workspace.updates.dismiss" : { "workspace.updates.dismiss" : {
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:521" ], "used-in" : [ "src/app/main/data/workspace/libraries.cljs:538" ],
"translations" : { "translations" : {
"en" : "Dismiss", "en" : "Dismiss",
"fr" : "", "fr" : "",
@ -3068,7 +3080,7 @@
} }
}, },
"workspace.updates.there-are-updates" : { "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" : { "translations" : {
"en" : "There are updates in shared libraries", "en" : "There are updates in shared libraries",
"fr" : "", "fr" : "",
@ -3077,7 +3089,7 @@
} }
}, },
"workspace.updates.update" : { "workspace.updates.update" : {
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:519" ], "used-in" : [ "src/app/main/data/workspace/libraries.cljs:536" ],
"translations" : { "translations" : {
"en" : "Update", "en" : "Update",
"fr" : "", "fr" : "",

View file

@ -78,3 +78,4 @@
@import 'main/partials/user-settings'; @import 'main/partials/user-settings';
@import 'main/partials/workspace'; @import 'main/partials/workspace';
@import 'main/partials/workspace-header'; @import 'main/partials/workspace-header';
@import 'main/partials/color-bullet';

View file

@ -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;
}
}

View file

@ -123,7 +123,7 @@
display: flex; display: flex;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: 4.8rem; height: 5rem;
padding: 0.25rem; padding: 0.25rem;
&.size-small { &.size-small {
@ -156,28 +156,6 @@
flex-basis: 52px; 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-text {
color: $color-gray-20; color: $color-gray-20;
font-size: $fs12; font-size: $fs12;
@ -186,11 +164,9 @@
text-overflow: ellipsis; text-overflow: ellipsis;
width: 66px; width: 66px;
text-align: center; text-align: center;
margin-top: 0.25rem;
} }
&.current { &.current {
.color {
border-color: $color-gray-50;
}
.color-text { .color-text {
color: $color-gray-50; color: $color-gray-50;
font-weight: bold; font-weight: bold;
@ -217,31 +193,11 @@
} }
&.add-color { &.add-color {
margin-left: 1.5rem; 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 { .color-text {
font-weight: bold; font-weight: bold;
} }
&:hover { &:hover {
.color {
border-color: $color-gray-30;
svg {
fill: $color-gray-30;
}
}
.color-text { .color-text {
color: $color-gray-40; color: $color-gray-40;
} }
@ -336,12 +292,5 @@ ul.palette-menu {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.color-bullet {
width: 20px;
height: 20px;
border-radius: 12px;
border: 1px solid $color-gray-10;
margin-right: 5px;
}
} }

View file

@ -29,7 +29,7 @@
border: none; border: none;
cursor: pointer; cursor: pointer;
&.active, &.active svg,
&:hover svg { &:hover svg {
fill: $color-primary; fill: $color-primary;
} }
@ -72,10 +72,16 @@
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
.gradient-background { .gradient-background-wrapper {
height: 100%; height: 100%;
width: 100%; width: 100%;
border: 1px solid $color-gray-10; 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 { .gradient-stop-wrapper {
@ -85,16 +91,21 @@
} }
.gradient-stop { .gradient-stop {
display: grid;
grid-template-columns: 50% 50%;
position: absolute; position: absolute;
width: 14px; width: 15px;
height: 14px; height: 15px;
border-radius: 2px; border-radius: 2px;
border: 1px solid $color-gray-20; border: 1px solid $color-gray-20;
margin-top: -2px; margin-top: -2px;
margin-left: -7px; margin-left: -7px;
box-shadow: 0 2px 2px rgb(0 0 0 / 15%); 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; 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 { .shade-selector {
display: grid; display: grid;
justify-items: center; justify-items: center;
@ -269,6 +271,10 @@
justify-items: center; justify-items: center;
grid-column-gap: 0.25rem; grid-column-gap: 0.25rem;
&.disable-opacity {
grid-template-columns: 3.5rem repeat(3, 1fr);
}
input { input {
width: 100%; width: 100%;
margin: 0; margin: 0;
@ -325,34 +331,6 @@
content: ""; content: "";
flex: auto; 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 { .actions {
@ -465,13 +443,10 @@
.colorpicker-tabs { .colorpicker-tabs {
display: flex; display: flex;
margin-top: 0.25rem; margin: 0.5rem 0;
border-radius: 5px;
border: 1px solid $color-gray-10;
height: 2rem; height: 2rem;
background-color: $color-gray-10;
.active {
background-color: $color-white;
}
.colorpicker-tab { .colorpicker-tab {
cursor: pointer; cursor: pointer;
@ -483,9 +458,21 @@
svg { svg {
width: 16px; width: 16px;
height: 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;
}
} }
} }

View file

@ -241,13 +241,6 @@
color: $color-white; color: $color-white;
cursor: pointer; cursor: pointer;
& .color-block {
width: 20px;
height: 20px;
border-radius: 10px;
margin-right: $x-small;
}
& span { & span {
margin-left: $x-small; margin-left: $x-small;
color: $color-gray-30; color: $color-gray-30;

View file

@ -467,47 +467,6 @@
margin-bottom: 0; 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 { .presets {
.custom-select-dropdown { .custom-select-dropdown {
width: 200px; width: 200px;

View file

@ -104,27 +104,26 @@
(assoc-in [:workspace-local :picked-color-select] value) (assoc-in [:workspace-local :picked-color-select] value)
(assoc-in [:workspace-local :picked-shift?] shift?))))) (assoc-in [:workspace-local :picked-shift?] shift?)))))
(defn change-fill (defn change-fill
([ids color id file-id] ([ids color]
(change-fill ids color 1 id file-id))
([ids color opacity id file-id]
(ptk/reify ::change-fill (ptk/reify ::change-fill
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(let [pid (:current-page-id state) (let [pid (:current-page-id state)
objects (get-in state [:workspace-data :pages-index pid :objects]) 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) ids (into ids children)
is-text? #(= :text (:type (get objects %))) is-text? #(= :text (:type (get objects %)))
text-ids (filter is-text? ids) text-ids (filter is-text? ids)
shape-ids (filter (comp not is-text?) ids) shape-ids (filter (comp not is-text?) ids)
attrs (cond-> {:fill-color color attrs (cond-> {:fill-color (:color color)
:fill-color-ref-id id :fill-color-ref-id (:id color)
:fill-color-ref-file file-id} :fill-color-ref-file (:file-id color)
(and opacity (not= opacity :multiple)) (assoc :fill-opacity opacity)) :fill-color-gradient (:gradient color)
:fill-opacity (:opacity color)})
update-fn (fn [shape] (merge shape attrs)) update-fn (fn [shape] (merge shape attrs))
editors (get-in state [:workspace-local :editors]) editors (get-in state [:workspace-local :editors])
@ -135,20 +134,22 @@
(map #(dwt/update-text-attrs {:id % :editor (get editors %) :attrs attrs}) text-ids) (map #(dwt/update-text-attrs {:id % :editor (get editors %) :attrs attrs}) text-ids)
(dwc/update-shapes shape-ids update-fn)))))))) (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/reify ::change-stroke
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(let [objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects]) (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) ids (into ids children)
update-fn (fn [s] update-fn (fn [s]
(cond-> s (cond-> s
true true
(assoc :stroke-color color (assoc :stroke-color (:color color)
:stroke-color-ref-id id :stroke-color-gradient (:gradient color)
:stroke-color-ref-file file-id) :stroke-color-ref-id (:id color)
:stroke-color-ref-file (:file-id color))
(= (:stroke-style s) :none) (= (:stroke-style s) :none)
(assoc :stroke-style :solid (assoc :stroke-style :solid
@ -157,20 +158,67 @@
(rx/of (dwc/update-shapes ids update-fn)))))) (rx/of (dwc/update-shapes ids update-fn))))))
(defn picker-for-selected-shape [] (defn picker-for-selected-shape []
;; TODO: replace st/emit! by a subject push and set that in the WatchEvent (let [sub (rx/subject)]
(let [handle-change-color (fn [color opacity id file-id shift?] (ptk/reify ::picker-for-selected-shape
(let [ids (get-in @st/state [:workspace-local :selected])] ptk/WatchEvent
(st/emit! (watch [_ state stream]
(if shift? (let [ids (get-in state [:workspace-local :selected])
(change-stroke ids color nil nil) stop? (->> stream
(change-fill ids color nil nil)) (rx/filter (ptk/type? ::stop-picker)))
(md/hide))))]
(ptk/reify ::start-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 ptk/UpdateEvent
(update [_ state] (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 (-> state
(assoc-in [:workspace-local :picking-color?] true) (assoc-in [:workspace-local :current-gradient] gradient)
(assoc ::md/modal {:id (random-uuid) (assoc-in [:workspace-local :current-gradient :shape-id] id))))))
:type :colorpicker
:props {:on-change handle-change-color} (defn stop-gradient []
:allow-click-outside true})))))) (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)))))

View file

@ -29,6 +29,14 @@
:type type :type type
:props props :props props
:allow-click-outside false}))))) :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 (defn hide
[] []
@ -42,12 +50,18 @@
(ptk/reify ::update-modal (ptk/reify ::update-modal
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(c/update state ::modal merge options)))) (cond-> state
(::modal state)
(c/update ::modal merge options)))))
(defn show! (defn show!
[type props] [type props]
(st/emit! (show type props))) (st/emit! (show type props)))
(defn update-props!
[type props]
(st/emit! (update-props type props)))
(defn allow-click-outside! (defn allow-click-outside!
[] []
(st/emit! (update {:allow-click-outside true}))) (st/emit! (update {:allow-click-outside true})))

View file

@ -741,7 +741,7 @@
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:workspace-local :selected])] (let [selected (get-in state [:workspace-local :selected])]
(rx/of (delete-shapes selected) (rx/of (delete-shapes selected)
dws/deselect-all))))) (dws/deselect-all))))))
;; --- Shape Vertical Ordering ;; --- Shape Vertical Ordering
@ -1318,7 +1318,7 @@
:grow-type (if (> (count text) 100) :auto-height :auto-width) :grow-type (if (> (count text) 100) :auto-height :auto-width)
:content (as-content text)})] :content (as-content text)})]
(rx/of dwc/start-undo-transaction (rx/of dwc/start-undo-transaction
dws/deselect-all (dws/deselect-all)
(add-shape shape) (add-shape shape)
(dwc/rehash-shape-frame-relationship [id]) (dwc/rehash-shape-frame-relationship [id])
dwc/commit-undo-transaction))))) dwc/commit-undo-transaction)))))
@ -1444,22 +1444,23 @@
(defn change-canvas-color (defn change-canvas-color
[color] [color]
(s/assert string? color) ;; TODO: Create a color spec
#_(s/assert string? color)
(ptk/reify ::change-canvas-color (ptk/reify ::change-canvas-color
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [page-id (get state :current-page-id) (let [page-id (get state :current-page-id)
options (dwc/lookup-page-options state page-id) options (dwc/lookup-page-options state page-id)
ccolor (:background options)] previus-color (:background options)]
(rx/of (dwc/commit-changes (rx/of (dwc/commit-changes
[{:type :set-option [{:type :set-option
:page-id page-id :page-id page-id
:option :background :option :background
:value color}] :value (:color color)}]
[{:type :set-option [{:type :set-option
:page-id page-id :page-id page-id
:option :background :option :background
:value ccolor}] :value previus-color}]
{:commit-local? true})))))) {:commit-local? true}))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1530,7 +1531,7 @@
(select-for-drawing :text)) (select-for-drawing :text))
"ctrl+c" #(st/emit! copy-selected) "ctrl+c" #(st/emit! copy-selected)
"ctrl+v" #(st/emit! paste) "ctrl+v" #(st/emit! paste)
"escape" #(st/emit! :interrupt deselect-all) "escape" #(st/emit! :interrupt (deselect-all true))
"del" #(st/emit! delete-selected) "del" #(st/emit! delete-selected)
"backspace" #(st/emit! delete-selected) "backspace" #(st/emit! delete-selected)
"ctrl+up" #(st/emit! (vertical-order-selected :up)) "ctrl+up" #(st/emit! (vertical-order-selected :up))

View file

@ -299,7 +299,7 @@
(rx/of dwc/start-undo-transaction) (rx/of dwc/start-undo-transaction)
(rx/empty)) (rx/empty))
(rx/of dw/deselect-all (rx/of (dw/deselect-all)
(dw/add-shape shape)))))))))) (dw/add-shape shape))))))))))
(def close-drawing-path (def close-drawing-path

View file

@ -21,7 +21,7 @@
(defonce ^:private default-square-params (defonce ^:private default-square-params
{:size 16 {:size 16
:color {:value "#59B9E2" :color {:color "#59B9E2"
:opacity 0.2}}) :opacity 0.2}})
(defonce ^:private default-layout-params (defonce ^:private default-layout-params
@ -30,7 +30,7 @@
:item-length nil :item-length nil
:gutter 8 :gutter 8
:margin 0 :margin 0
:color {:value "#DE4762" :color {:color "#DE4762"
:opacity 0.1}}) :opacity 0.1}})
(defonce default-grid-params (defonce default-grid-params

View file

@ -33,25 +33,32 @@
(declare sync-file) (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 (defn add-color
[color] [color]
(us/assert ::us/string color) (let [id (uuid/next)
(ptk/reify ::add-color color (assoc color
ptk/WatchEvent :id id
(watch [_ state s] :name (default-color-name color))]
(let [id (uuid/next) (us/assert ::cp/color color)
rchg {:type :add-color (ptk/reify ::add-color
:color {:id id ptk/WatchEvent
:name color (watch [_ state s]
:value color}} (let [rchg {:type :add-color
uchg {:type :del-color :color color}
:id id}] uchg {:type :del-color
(rx/of #(assoc-in % [:workspace-local :color-for-rename] id) :id id}]
(dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) (rx/of #(assoc-in % [:workspace-local :color-for-rename] id)
(dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))))
(defn add-recent-color (defn add-recent-color
[color] [color]
(us/assert ::us/string color) (us/assert ::cp/recent-color color)
(ptk/reify ::add-recent-color (ptk/reify ::add-recent-color
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]

View file

@ -22,6 +22,7 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.modal :as md]
[app.main.streams :as ms] [app.main.streams :as ms]
[app.main.worker :as uw])) [app.main.worker :as uw]))
@ -66,7 +67,7 @@
(ms/mouse-up? %)) (ms/mouse-up? %))
stream)] stream)]
(rx/concat (rx/concat
(rx/of deselect-all) (rx/of (deselect-all))
(->> ms/mouse-position (->> ms/mouse-position
(rx/scan (fn [data pos] (rx/scan (fn [data pos]
(if data (if data
@ -117,15 +118,26 @@
objects (dwc/lookup-page-objects state page-id)] objects (dwc/lookup-page-objects state page-id)]
(rx/of (dwc/expand-all-parents ids objects)))))) (rx/of (dwc/expand-all-parents ids objects))))))
(def deselect-all (defn deselect-all
"Clear all possible state of drawing, edition "Clear all possible state of drawing, edition
or any similar action taken by the user." or any similar action taken by the user.
(ptk/reify ::deselect-all When `check-modal` the method will check if a modal is opened
ptk/UpdateEvent and not deselect if it's true"
(update [_ state] ([] (deselect-all false))
(update state :workspace-local #(-> %
(assoc :selected (d/ordered-set)) ([check-modal]
(dissoc :selected-frame)))))) (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) ;; --- Select Shapes (By selrect)

View file

@ -26,7 +26,8 @@
[app.main.ui.shapes.path :as path] [app.main.ui.shapes.path :as path]
[app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.rect :as rect]
[app.main.ui.shapes.text :as text] [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 (def ^:private default-color "#E8E9EA") ;; $color-canvas
@ -56,7 +57,8 @@
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(let [childs (mapv #(get objects %) (:shapes shape)) (let [childs (mapv #(get objects %) (:shapes shape))
shape (geom/transform-shape 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 (defn group-wrapper-factory
[objects] [objects]
@ -78,10 +80,8 @@
frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))]
(when (and shape (not (:hidden shape))) (when (and shape (not (:hidden shape)))
(let [shape (geom/transform-shape frame shape) (let [shape (geom/transform-shape frame shape)
opts #js {:shape shape} opts #js {:shape shape}]
filter-id (filters/get-filter-id)] [:> shape-container {:shape shape}
[:g {:filter (filters/filter-str filter-id shape)}
[:& filters/filters {:filter-id filter-id :shape shape}]
(case (:type shape) (case (:type shape)
:curve [:> path/path-shape opts] :curve [:> path/path-shape opts]
:text [:> text/text-shape opts] :text [:> text/text-shape opts]

View file

@ -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])))

View file

@ -12,3 +12,5 @@
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(def embed-ctx (mf/create-context false)) (def embed-ctx (mf/create-context false))
(def render-ctx (mf/create-context nil))

View file

@ -20,10 +20,10 @@
(:import goog.events.EventType)) (:import goog.events.EventType))
(defn- on-esc-clicked (defn- on-esc-clicked
[event] [event allow-click-outside]
(when (k/esc? event) (when (and (k/esc? event) (not allow-click-outside))
(st/emit! (dm/hide)) (do (dom/stop-propagation event)
(dom/stop-propagation event))) (st/emit! (dm/hide)))))
(defn- on-pop-state (defn- on-pop-state
[event] [event]
@ -43,11 +43,14 @@
(st/emit! (dm/hide))))) (st/emit! (dm/hide)))))
(defn- on-click-outside (defn- on-click-outside
[event wrapper-ref allow-click-outside] [event wrapper-ref type allow-click-outside]
(let [wrapper (mf/ref-val wrapper-ref) (let [wrapper (mf/ref-val wrapper-ref)
current (dom/get-target event)] 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/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(st/emit! (dm/hide))))) (st/emit! (dm/hide)))))
@ -59,16 +62,23 @@
(let [data (unchecked-get props "data") (let [data (unchecked-get props "data")
wrapper-ref (mf/use-ref nil) wrapper-ref (mf/use-ref nil)
allow-click-outside (:allow-click-outside data)
handle-click-outside handle-click-outside
(fn [event] (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/use-layout-effect
(mf/deps allow-click-outside)
(fn [] (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/window EventType.POPSTATE on-pop-state)
(events/listen js/document EventType.CLICK handle-click-outside)]] (events/listen js/document EventType.CLICK handle-click-outside)]]
#(for [key keys] #(doseq [key keys]
(events/unlistenByKey key))))) (events/unlistenByKey key)))))
[:div.modal-wrapper {:ref wrapper-ref} [:div.modal-wrapper {:ref wrapper-ref}

View file

@ -9,8 +9,10 @@
(ns app.main.ui.shapes.attrs (ns app.main.ui.shapes.attrs
(:require (:require
[rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
[app.util.object :as obj])) [app.util.object :as obj]
[app.main.ui.context :as muc]))
(defn- stroke-type->dasharray (defn- stroke-type->dasharray
[style] [style]
@ -20,21 +22,37 @@
:dashed "10,10" :dashed "10,10"
nil)) 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 (defn extract-style-attrs
([shape] (extract-style-attrs shape nil)) ([shape]
([shape gradient-id] (let [render-id (mf/use-ctx muc/render-ctx)]
(let [stroke-style (:stroke-style shape :none) (-> (obj/new)
attrs #js {:rx (:rx shape nil) (add-border-radius shape)
:ry (:ry shape nil)} (add-fill shape render-id)
attrs (obj/merge! attrs (add-stroke shape render-id)))))
(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)))

View file

@ -25,8 +25,9 @@
(str/fmt "url(#$0)" [filter-id]))) (str/fmt "url(#$0)" [filter-id])))
(mf/defc color-matrix (mf/defc color-matrix
[{:keys [color opacity]}] [{:keys [color]}]
(let [[r g b a] (color/hex->rgba color opacity) (let [{:keys [color opacity]} color
[r g b a] (color/hex->rgba color opacity)
[r g b] [(/ r 255) (/ g 255) (/ b 255)]] [r g b] [(/ r 255) (/ g 255) (/ b 255)]]
[:feColorMatrix [:feColorMatrix
{:type "matrix" {:type "matrix"
@ -36,7 +37,7 @@
[{:keys [filter-id filter shape]}] [{:keys [filter-id filter shape]}]
(let [{:keys [x y width height]} (:selrect 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" [: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"}] :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}] [:feOffset {:dx offset-x :dy offset-y}]
[:feGaussianBlur {:stdDeviation (/ blur 2)}] [:feGaussianBlur {:stdDeviation (/ blur 2)}]
[:& color-matrix {:color color :opacity opacity}] [:& color-matrix {:color color}]
[:feBlend {:mode "normal" [:feBlend {:mode "normal"
:in2 in-filter :in2 in-filter
@ -58,7 +59,7 @@
[{:keys [filter-id filter shape]}] [{:keys [filter-id filter shape]}]
(let [{:keys [x y width height]} (:selrect 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" [: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" :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" :k2 "-1"
:k3 "1"}] :k3 "1"}]
[:& color-matrix {:color color :opacity opacity}] [:& color-matrix {:color color}]
[:feBlend {:mode "normal" [:feBlend {:mode "normal"
:in2 in-filter :in2 in-filter
@ -122,45 +123,42 @@
[filter-x filter-y filter-width filter-height] (get-filters-bounds shape filters)] [filter-x filter-y filter-width filter-height] (get-filters-bounds shape filters)]
(when (seq filters) (when (seq filters)
[:defs [:filter {:id filter-id
[:filter {:id filter-id :x filter-x :y filter-y
:x filter-x :y filter-y :width filter-width :height filter-height
:width filter-width :height filter-height :filterUnits "userSpaceOnUse"
:filterUnits "userSpaceOnUse" :color-interpolation-filters "sRGB"}
:color-interpolation-filters "sRGB"}
(let [;; Add as a paramter the input filter (let [;; Add as a paramter the input filter
drop-shadow-filters (->> filters (filter #(= :drop-shadow (:style %)))) drop-shadow-filters (->> filters (filter #(= :drop-shadow (:style %))))
drop-shadow-filters (->> drop-shadow-filters drop-shadow-filters (->> drop-shadow-filters
(map #(str "filter" (:id %))) (map #(str "filter" (:id %)))
(cons "BackgroundImageFix") (cons "BackgroundImageFix")
(map add-in-filter drop-shadow-filters)) (map add-in-filter drop-shadow-filters))
inner-shadow-filters (->> filters (filter #(= :inner-shadow (:style %)))) inner-shadow-filters (->> filters (filter #(= :inner-shadow (:style %))))
inner-shadow-filters (->> inner-shadow-filters inner-shadow-filters (->> inner-shadow-filters
(map #(str "filter" (:id %))) (map #(str "filter" (:id %)))
(cons "shape") (cons "shape")
(map add-in-filter inner-shadow-filters))] (map add-in-filter inner-shadow-filters))]
[:* [:*
[:feFlood {:flood-opacity 0 :result "BackgroundImageFix"}] [:feFlood {:flood-opacity 0 :result "BackgroundImageFix"}]
(for [{:keys [id type] :as filter} drop-shadow-filters] (for [{:keys [id type] :as filter} drop-shadow-filters]
[:& drop-shadow-filter {:key id [:& 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-id filter-id
:filter filter :filter filter
:shape shape}]) :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}])
])
]])))

View file

@ -11,11 +11,12 @@
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
[goog.object :as gobj] [app.util.object :as obj]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.ui.context :as muc]
[app.common.geom.point :as gpt])) [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] (let [{:keys [x y width height]} shape]
[:defs [:defs
[:linearGradient {:id id [:linearGradient {:id id
@ -29,12 +30,12 @@
:stop-color color :stop-color color
:stop-opacity opacity}])]])) :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] (let [{:keys [x y width height]} shape]
[:defs [: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)))) (+ y (* height (:start-y gradient))))
gradient-vec (gpt/to-vec (gpt/point (* width (:start-x gradient)) gradient-vec (gpt/to-vec (gpt/point (* width (:start-x gradient))
(* height (:start-y gradient))) (* height (:start-y gradient)))
@ -50,8 +51,8 @@
scale-factor-x (* scale-factor-y (:width gradient)) scale-factor-x (* scale-factor-y (:width gradient))
scale-vec (gpt/point (* scale-factor-y (/ height 2)) 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-translate (str/fmt "translate(%s, %s)" (:x translate-vec) (:y translate-vec))
tr-rotate (str/fmt "rotate(%s)" angle) tr-rotate (str/fmt "rotate(%s)" angle)
tr-scale (str/fmt "scale(%s, %s)" (:x scale-vec) (:y scale-vec)) tr-scale (str/fmt "scale(%s, %s)" (:x scale-vec) (:y scale-vec))
@ -71,8 +72,15 @@
(mf/defc gradient (mf/defc gradient
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [gradient (gobj/get props "gradient")] (let [attr (obj/get props "attr")
(case (:type gradient) shape (obj/get props "shape")
:linear [:> linear-gradient props] render-id (mf/use-ctx muc/render-ctx)
:radial [:> radial-gradient props] id (str (name attr) "_" render-id)
nil))) 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]))))

View file

@ -27,9 +27,7 @@
{:keys [id x y width height]} shape {:keys [id x y width height]} shape
transform (geom/transform-matrix shape) transform (geom/transform-matrix shape)
gradient-id (when (:fill-color-gradient shape) (str (uuid/next))) props (-> (attrs/extract-style-attrs shape)
props (-> (attrs/extract-style-attrs shape gradient-id)
(obj/merge! (obj/merge!
#js {:x x #js {:x x
:y y :y y
@ -38,12 +36,6 @@
:width width :width width
:height height}))] :height height}))]
[:& shape-custom-stroke {:shape shape
[:* :base-props props
(when gradient-id :elem-name "rect"}]))
[:& gradient {:id gradient-id
:shape shape
:gradient (:fill-color-gradient shape)}])
[:& shape-custom-stroke {:shape shape
:base-props props
:elem-name "rect"}]]))

View file

@ -7,5 +7,33 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; 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]]))

View file

@ -68,17 +68,25 @@
fill-color (obj/get data "fill-color" fill) fill-color (obj/get data "fill-color" fill)
fill-opacity (obj/get data "fill-opacity" opacity) 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-id (obj/get data "fill-color-ref-id")
fill-color-ref-file (obj/get data "fill-color-ref-file") fill-color-ref-file (obj/get data "fill-color-ref-file")
[r g b a] (uc/hex->rgba fill-color fill-opacity) [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) fontsdb (deref fonts/fontsdb)
base #js {:textDecoration text-decoration base #js {:textDecoration text-decoration
:color (str/format "rgba(%s, %s, %s, %s)" r g b a)
:textTransform text-transform :textTransform text-transform
:lineHeight (or line-height "inherit")}] :lineHeight (or line-height "inherit")
"--text-color" background}]
(when (and (string? letter-spacing) (when (and (string? letter-spacing)
(pos? (alength letter-spacing))) (pos? (alength letter-spacing)))
@ -167,7 +175,7 @@
(if (string? text) (if (string? text)
(let [style (generate-text-styles (clj->js node))] (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] (let [children (map-indexed (fn [index node]
(mf/element text-node {:index index :node node :key index})) (mf/element text-node {:index index :node node :key index}))
children)] children)]
@ -179,13 +187,15 @@
{:key index {:key index
:style style :style style
:xmlns "http://www.w3.org/1999/xhtml"} :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]) children])
"paragraph-set" "paragraph-set"
(let [style #js {:display "inline-block"}] (let [style #js {:display "inline-block"}]
[:div.paragraphs {:key index :style style} children]) [:div.paragraphs {:key index :style style} children])
"paragraph" "paragraph"
(let [style (generate-paragraph-styles (clj->js node))] (let [style (generate-paragraph-styles (clj->js node))]

View file

@ -29,7 +29,8 @@
[app.util.object :as obj] [app.util.object :as obj]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [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 (defn on-mouse-down
[event {:keys [interactions] :as shape}] [event {:keys [interactions] :as shape}]
@ -54,14 +55,11 @@
on-mouse-down (mf/use-callback on-mouse-down (mf/use-callback
(mf/deps shape) (mf/deps shape)
#(on-mouse-down % shape)) #(on-mouse-down % shape))]
filter-id (filters/get-filter-id)] [:> shape-container {:shape shape
:on-mouse-down on-mouse-down
[:g.shape {:on-mouse-down on-mouse-down :cursor (when (:interactions shape) "pointer")}
:cursor (when (:interactions shape) "pointer")
:filter (filters/filter-str filter-id shape)}
[:& filters/filters {:filter-id filter-id :shape shape}]
[:& component {:shape shape [:& component {:shape shape
:frame frame :frame frame
:childs childs :childs childs

View file

@ -19,6 +19,7 @@
[app.main.data.workspace :as udw] [app.main.data.workspace :as udw]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.color-bullet :as cb]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.util.color :refer [hex->rgb]] [app.util.color :refer [hex->rgb]]
@ -55,14 +56,13 @@
(fn [event] (fn [event]
(let [ids (get-in @st/state [:workspace-local :selected])] (let [ids (get-in @st/state [:workspace-local :selected])]
(if (kbd/shift? event) (if (kbd/shift? event)
(st/emit! (mdc/change-stroke ids (:value color) id file-id)) (st/emit! (mdc/change-stroke ids color))
(st/emit! (mdc/change-fill ids (:value color) id file-id)))))] (st/emit! (mdc/change-fill ids color)))))]
[:div.color-cell {:class (str "cell-"(name size)) [:div.color-cell {:class (str "cell-"(name size))
:key (or (str (:id color)) (:value color))
:on-click select-color} :on-click select-color}
[:span.color {:style {:background (:value color)}}] [:& cb/color-bullet {:color color}]
(when (= size :big) [:span.color-text {:title (:name color) } (or (:name color) (:value color))])])) [:& cb/color-name {:color color :size size}]]))
(mf/defc palette (mf/defc palette
[{:keys [left-sidebar? current-colors recent-colors file-colors shared-libs selected size]}] [{:keys [left-sidebar? current-colors recent-colors file-colors shared-libs selected size]}]
@ -138,9 +138,9 @@
(when (= selected (:id cur-library)) i/tick) (when (= selected (:id cur-library)) i/tick)
[:div.library-name (str (:name cur-library) " " (str/format "(%s)" (count colors)))] [:div.library-name (str (:name cur-library) " " (str/format "(%s)" (count colors)))]
[:div.color-sample [:div.color-sample
(for [[idx {:keys [id value]}] (map-indexed vector (take 7 colors))] (for [[idx {:keys [id color]}] (map-indexed vector (take 7 colors))]
[:div.color-bullet {:key (str "color-" idx) [:& cb/color-bullet {:key (str "color-" idx)
:style {:background-color value}}])]])) :color color}])]]))
[:li.palette-library [:li.palette-library
@ -149,9 +149,9 @@
[:div.library-name (str (t locale "workspace.libraries.colors.file-library") [:div.library-name (str (t locale "workspace.libraries.colors.file-library")
(str/format " (%s)" (count file-colors)))] (str/format " (%s)" (count file-colors)))]
[:div.color-sample [:div.color-sample
(for [[idx {:keys [value]}] (map-indexed vector (take 7 (vals file-colors))) ] (for [[idx color] (map-indexed vector (take 7 (vals file-colors))) ]
[:div.color-bullet {:key (str "color-" idx) [:& cb/color-bullet {:key (str "color-" idx)
:style {:background-color value}}])]] :color color}])]]
[:li.palette-library [:li.palette-library
{:on-click #(st/emit! (mdc/change-palette-selected :recent))} {:on-click #(st/emit! (mdc/change-palette-selected :recent))}
@ -159,9 +159,9 @@
[:div.library-name (str (t locale "workspace.libraries.colors.recent-colors") [:div.library-name (str (t locale "workspace.libraries.colors.recent-colors")
(str/format " (%s)" (count recent-colors)))] (str/format " (%s)" (count recent-colors)))]
[:div.color-sample [:div.color-sample
(for [[idx value] (map-indexed vector (take 7 (reverse recent-colors))) ] (for [[idx color] (map-indexed vector (take 7 (reverse recent-colors))) ]
[:div.color-bullet {:key (str "color-" idx) [:& cb/color-bullet {:key (str "color-" idx)
:style {:background-color value}}])]] :color color}])]]
[:hr.dropdown-separator] [:hr.dropdown-separator]
@ -191,14 +191,8 @@
[:span.right-arrow {:on-click on-right-arrow-click} i/arrow-slide]])) [: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] (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])))) (vals (get-in shared-libs [selected :data :colors]))))
(mf/defc colorpalette (mf/defc colorpalette
@ -217,21 +211,21 @@
(reset! current-library-colors (reset! current-library-colors
(into [] (into []
(cond (cond
(= selected :recent) (recent->colors recent-colors) (= selected :recent) (reverse recent-colors)
(= selected :file) (file->colors file-colors) (= selected :file) (vals file-colors)
:else (library->colors shared-libs selected)))))) :else (library->colors shared-libs selected))))))
(mf/use-effect (mf/use-effect
(mf/deps recent-colors) (mf/deps recent-colors)
(fn [] (fn []
(when (= selected :recent) (when (= selected :recent)
(reset! current-library-colors (into [] (recent->colors recent-colors)))))) (reset! current-library-colors (reverse recent-colors)))))
(mf/use-effect (mf/use-effect
(mf/deps file-colors) (mf/deps file-colors)
(fn [] (fn []
(when (= selected :file) (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? [:& palette {:left-sidebar? left-sidebar?
:current-colors @current-library-colors :current-colors @current-library-colors

View file

@ -21,10 +21,16 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.data.workspace.libraries :as dwl] [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.data.modal :as modal]
[app.main.ui.icons :as i] [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 ;; --- Refs
@ -43,414 +49,15 @@
(def viewport (def viewport
(l/derived (l/in [:workspace-local :vport]) st/state)) (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 ;; --- Color Picker Modal
(mf/defc value-saturation-selector [{:keys [hue saturation value on-change]}] (defn color->components [value opacity]
(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]
(let [value (if (uc/hex? value) value "#000000") (let [value (if (uc/hex? value) value "#000000")
[r g b] (uc/hex->rgb value) [r g b] (uc/hex->rgb value)
[h s v] (uc/hex->hsv value)] [h s v] (uc/hex->hsv value)]
@ -460,58 +67,139 @@
:r r :g g :b b :r r :g g :b b
:h h :s s :v v})) :h h :s s :v v}))
(mf/defc colorpicker (defn data->state [{:keys [color opacity gradient]}]
[{:keys [value opacity on-change on-accept]}] (let [type (cond
(let [current-color (mf/use-state (as-color-components value opacity)) (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) active-tab (mf/use-state :ramp #_:harmony #_:hsva)
selected-library (mf/use-state "recent") locale (mf/deref i18n/locale)
current-library-colors (mf/use-state [])
ref-picker (mf/use-ref) ref-picker (mf/use-ref)
file-colors (mf/deref refs/workspace-file-colors) dirty? (mf/use-var false)
shared-libs (mf/deref refs/workspace-libraries) last-color (mf/use-var data)
recent-colors (mf/deref refs/workspace-recent-colors)
picking-color? (mf/deref picking-color?) picking-color? (mf/deref picking-color?)
picked-color (mf/deref picked-color) picked-color (mf/deref picked-color)
picked-color-select (mf/deref picked-color-select) picked-color-select (mf/deref picked-color-select)
picked-shift? (mf/deref picked-shift?) 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] handle-change-color
(if (#{"recent" "file"} selected) (fn [changes]
(keyword selected) (let [editing-stop (:editing-stop @state)]
(uuid selected)) ) (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] handle-change-stop
(swap! current-color merge changes) (fn [offset]
(when (:hex changes) (when-let [offset-color (get-in @state [:stops offset])]
(reset! value-ref (:hex changes))) (do (swap! state assoc
(on-change (:hex changes (:hex @current-color)) :current-color offset-color
(:alpha changes (:alpha @current-color))))] :editing-stop offset)
;; Update state when there is a change in the props upstream (st/emit! (dc/select-gradient-stop offset)))))
(mf/use-effect
(mf/deps value opacity) on-select-library-color
(fn [] (fn [color]
(reset! current-color (as-color-components value opacity)))) (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 ;; Updates the CSS color variable when there is a change in the color
(mf/use-effect (mf/use-effect
(mf/deps @current-color) (mf/deps current-color)
(fn [] (let [node (mf/ref-val ref-picker) (fn [] (let [node (mf/ref-val ref-picker)
rgb [(:r @current-color) (:g @current-color) (:b @current-color)] {:keys [r g b h s v]} current-color
hue-rgb (uc/hsv->rgb [(:h @current-color) 1.0 255]) rgb [r g b]
hsl-from (uc/hsv->hsl [(:h @current-color) 0 (:v @current-color)]) hue-rgb (uc/hsv->rgb [h 1.0 255])
hsl-to (uc/hsv->hsl [(:h @current-color) 1 (:v @current-color)]) hsl-from (uc/hsv->hsl [h 0.0 v])
hsl-to (uc/hsv->hsl [h 1.0 v])
format-hsl (fn [[h s l]] format-hsl (fn [[h s l]]
(str/fmt "hsl(%s, %s, %s)" (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-from" (format-hsl hsl-from))
(dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to))))) (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 ;; When closing the modal we update the recent-color list
(mf/use-effect (mf/use-effect
(fn [] (fn [] #(fn []
(st/emit! (dwc/stop-picker)) (st/emit! (dc/stop-picker))
(when @value-ref (when @last-color
(st/emit! (dwl/add-recent-color @value-ref)))))) (st/emit! (dwl/add-recent-color @last-color)))))
(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?))))))
;; Updates color when used el pixel picker
(mf/use-effect (mf/use-effect
(mf/deps picking-color? picked-color-select) (mf/deps picking-color? picked-color-select)
(fn [] (when (and picking-color? picked-color-select) (fn []
(on-change (:hex @current-color) (:alpha @current-color) nil nil picked-shift?)))) (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 {:ref ref-picker}
[:div.colorpicker-content [:div.colorpicker-content
[:div.top-actions [:div.top-actions
[:button.picker-btn [:button.picker-btn
{:class (when picking-color? "active") {:class (when picking-color? "active")
:on-click (fn [] :on-click handle-click-picker}
(modal/allow-click-outside!)
(st/emit! (dwc/start-picker)))}
i/picker] i/picker]
[:div.gradients-buttons (when (not disable-gradient)
[:button.gradient.linear-gradient #_{:class "active"}] [:div.gradients-buttons
[:button.gradient.radial-gradient]]] [:button.gradient.linear-gradient
{:on-click (on-activate-gradient :linear-gradient)
:class (when (= :linear-gradient (:type @state)) "active")}]
#_[:div.gradient-stops [:button.gradient.radial-gradient
[:div.gradient-background {:style {:background "linear-gradient(90deg, #EC0BE5, #CDCDCD)" }}] {:on-click (on-activate-gradient :radial-gradient)
[:div.gradient-stop-wrapper :class (when (= :radial-gradient (:type @state)) "active")}]])]
[:div.gradient-stop.start {:style {:background-color "#EC0BE5"}}]
[:div.gradient-stop.end {:style {:background-color "#CDCDCD" [:& gradients {:type (:type @state)
:left "100%"}}]]] :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? (if picking-color?
[:div.picker-detail-wrapper [:div.picker-detail-wrapper
[:div.center-circle] [:div.center-circle]
[:canvas#picker-detail {:width 200 :height 160}]] [:canvas#picker-detail {:width 200 :height 160}]]
(case @active-tab (case @active-tab
:ramp [:& ramp-selector {:color @current-color :on-change handle-change-color}] :ramp [:& ramp-selector {:color current-color
:harmony [:& harmony-selector {:color @current-color :on-change handle-change-color}] :disable-opacity disable-opacity
:hsva [:& hsva-selector {:color @current-color :on-change handle-change-color}] :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)) 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 [:& libraries {:current-color current-color
[:select {:on-change (fn [e] :disable-gradient disable-gradient
(let [val (-> e dom/get-target dom/get-value)] :disable-opacity disable-opacity
(reset! selected-library val))) :on-select-color on-select-library-color
:value @selected-library} :on-add-library-color on-add-library-color}]
[: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 on-accept
(when (= "file" @selected-library) [:div.actions
[:div.color-bullet.button.plus-button {:style {:background-color "white"} [:button.btn-primary.btn-large
:on-click #(st/emit! (dwl/add-color (:hex @current-color)))} {:on-click (fn []
i/plus]) (on-accept (state->data @state))
(modal/hide!))}
[:div.color-bullet.button {:style {:background-color "white"} (t locale "workspace.libraries.colors.save-color")]])]]))
: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")]])])
)
(defn calculate-position (defn calculate-position
"Calculates the style properties for the given coordinates and position" "Calculates the style properties for the given coordinates and position"
@ -673,31 +355,32 @@
(mf/defc colorpicker-modal (mf/defc colorpicker-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :colorpicker} ::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) (let [vport (mf/deref viewport)
dirty? (mf/use-var false) dirty? (mf/use-var false)
last-change (mf/use-var nil) last-change (mf/use-var nil)
position (or position :left) position (or position :left)
style (calculate-position vport position x y) style (calculate-position vport position x y)
handle-change (fn [new-value new-opacity id file-id shift-clicked?] handle-change (fn [new-data shift-clicked?]
(when (or (not= new-value value) (not= new-opacity opacity)) (reset! dirty? (not= data new-data))
(reset! dirty? true)) (reset! last-change new-data)
(reset! last-change [new-value new-opacity id file-id])
(when on-change (when on-change
(on-change new-value new-opacity id file-id shift-clicked?)))] (on-change new-data)))]
(mf/use-effect (mf/use-effect
(fn [] (fn []
#(when (and @dirty? on-close) #(when (and @dirty? @last-change on-close)
(when-let [[value opacity id file-id] @last-change] (on-close @last-change))))
(on-close value opacity id file-id)))))
[:div.colorpicker-tooltip [:div.colorpicker-tooltip
{:style (clj->js style)} {:style (clj->js style)}
[:& colorpicker {:value (or value default) [:& colorpicker {:data data
:opacity (or opacity 1) :disable-gradient disable-gradient
:disable-opacity disable-opacity
:on-change handle-change :on-change handle-change
:on-accept on-accept :on-accept on-accept}]]))
:disable-opacity disable-opacity}]]))

View file

@ -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"])]))

View file

@ -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)}}]])])]]))

View file

@ -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}])]]))

View file

@ -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}]])]))

View file

@ -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)}])]]))

View file

@ -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]]]]))

View file

@ -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]]))

View file

@ -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}])]]))

View file

@ -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)}])]))

View file

@ -18,7 +18,9 @@
(mf/defc square-grid [{:keys [frame zoom grid] :as props}] (mf/defc square-grid [{:keys [frame zoom grid] :as props}]
(let [{:keys [color size] :as params} (-> grid :params) (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] {frame-width :width frame-height :height :keys [x y]} frame]
(when (> size 0) (when (> size 0)
[:g.grid [:g.grid
@ -43,7 +45,9 @@
:stroke-width (str (/ 1 zoom))}}])]]))) :stroke-width (str (/ 1 zoom))}}])]])))
(mf/defc layout-grid [{:keys [key frame zoom grid]}] (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 (-> grid :params :gutter)
gutter? (and (not (nil? gutter)) (not= gutter 0)) gutter? (and (not (nil? gutter)) (not= gutter 0))

View file

@ -13,13 +13,17 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
[beicon.core :as rx] [beicon.core :as rx]
[app.main.data.workspace.common :as dwc] [okulary.core :as l]
[app.main.store :as st]
[app.main.streams :as ms]
[app.common.math :as mth] [app.common.math :as mth]
[app.util.dom :as dom]
[app.common.geom.point :as gpt] [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-width 2)
(def gradient-line-stroke-color "white") (def gradient-line-stroke-color "white")
@ -31,6 +35,12 @@
(def gradient-square-stroke-color "white") (def gradient-square-stroke-color "white")
(def gradient-square-stroke-color-selected "#1FDEA7") (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]}] (mf/defc shadow [{:keys [id x y width height offset]}]
[:filter {:id id [:filter {:id id
:x x :x x
@ -77,24 +87,13 @@
:height (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4) :height (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4)
:offset (/ 2 zoom)}]) :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,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAACvUlEQVQoFQGyAk39AeLi4gAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB////AAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjScaa0cU7nIAAAAASUVORK5CYII=")
#_(def checkboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") #_(def checkboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
(mf/defc gradient-color-handler (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) [:g {:filter (str/fmt "url(#%s)" filter-id)
:transform (gmt/rotate-matrix angle point)} :transform (gmt/rotate-matrix angle point)}
@ -114,12 +113,13 @@
:on-mouse-down (partial on-mouse-down :to-p) :on-mouse-down (partial on-mouse-down :to-p)
:on-mouse-up (partial on-mouse-up :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)) :y (- (:y point) (/ gradient-square-width 2 zoom))
:rx (/ gradient-square-radius zoom) :rx (/ gradient-square-radius zoom)
:width (/ gradient-square-width zoom) :width (/ gradient-square-width zoom)
:height (/ gradient-square-width zoom) :height (/ gradient-square-width zoom)
:stroke "white" :stroke (if selected "#31EFB8" "white")
:stroke-width (/ gradient-square-stroke-width zoom) :stroke-width (/ gradient-square-stroke-width zoom)
:fill (:value color) :fill (:value color)
:fill-opacity (:opacity color) :fill-opacity (:opacity color)
@ -128,18 +128,27 @@
:on-mouse-up on-mouse-up}]]) :on-mouse-up on-mouse-up}]])
(mf/defc gradient-handler-transformed (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) (let [moving-point (mf/use-var nil)
angle (+ 90 (gpt/angle from-p to-p)) angle (+ 90 (gpt/angle from-p to-p))
on-click (fn [position event] on-click (fn [position event]
(dom/stop-propagation 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] on-mouse-down (fn [position event]
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default 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] on-mouse-up (fn [position event]
(dom/stop-propagation event) (dom/stop-propagation event)
@ -192,7 +201,8 @@
(when width-p (when width-p
[:g {:filter "url(#gradient_width_handler_drop_shadow)"} [: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) :cy (:y width-p)
:r (/ gradient-width-handler-radius zoom) :r (/ gradient-width-handler-radius zoom)
:fill gradient-width-handler-color :fill gradient-width-handler-color
@ -200,7 +210,8 @@
:on-mouse-up (partial on-mouse-up :width-p)}]]) :on-mouse-up (partial on-mouse-up :width-p)}]])
[:& gradient-color-handler [:& 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 :zoom zoom
:point from-p :point from-p
:color from-color :color from-color
@ -210,7 +221,8 @@
:on-mouse-up (partial on-mouse-up :from-p)}] :on-mouse-up (partial on-mouse-up :from-p)}]
[:& gradient-color-handler [:& gradient-color-handler
{:filter-id "gradient_square_to_drop_shadow" {:selected (= editing 1)
:filter-id "gradient_square_to_drop_shadow"
:zoom zoom :zoom zoom
:point to-p :point to-p
:color to-color :color to-color
@ -219,74 +231,70 @@
:on-mouse-down (partial on-mouse-down :to-p) :on-mouse-down (partial on-mouse-down :to-p)
:on-mouse-up (partial on-mouse-up :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} [{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))) from-p (gpt/point (+ x (* width (:start-x gradient)))
(+ y (* height (:start-y @state)))) (+ y (* height (:start-y gradient))))
to-p (gpt/point (+ x (* width (:end-x @state))) to-p (gpt/point (+ x (* width (:end-x gradient)))
(+ y (* height (:end-y @state)))) (+ y (* height (:end-y gradient))))
gradient-vec (gpt/to-vec from-p to-p) gradient-vec (gpt/to-vec from-p to-p)
gradient-length (gpt/length gradient-vec) gradient-length (gpt/length gradient-vec)
width-v (-> gradient-vec width-v (-> gradient-vec
(gpt/normal-left) (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)))) (gpt/multiply (gpt/point (/ width 2))))
width-p (gpt/add from-p width-v) width-p (gpt/add from-p width-v)
change! (fn [changes]
(st/emit! (dc/update-gradient changes)))
on-change-start (fn [point] on-change-start (fn [point]
(let [start-x (/ (- (:x point) x) width) (let [start-x (/ (- (:x point) x) width)
start-y (/ (- (:y point) y) height)] start-y (/ (- (:y point) y) height)
(swap! state assoc start-x (mth/precision start-x 2)
:start-x start-x start-y (mth/precision start-y 2)]
:start-y start-y ))) (change! {:start-x start-x :start-y start-y})))
on-change-finish (fn [point] on-change-finish (fn [point]
(let [end-x (/ (- (:x point) x) width) (let [end-x (/ (- (:x point) x) width)
end-y (/ (- (:y point) y) height)] end-y (/ (- (:y point) y) height)
(swap! state assoc
:end-x end-x end-x (mth/precision end-x 2)
:end-y end-y))) end-y (mth/precision end-y 2)]
(change! {:end-x end-x :end-y end-y})))
on-change-width (fn [point] on-change-width (fn [point]
(let [scale-factor-y (/ gradient-length (/ height 2)) (let [scale-factor-y (/ gradient-length (/ height 2))
norm-dist (/ (gpt/distance point from-p) norm-dist (/ (gpt/distance point from-p)
(* (/ width 2) scale-factor-y))] (* (/ 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 (when (and gradient
(mf/deps shape) (= id (:shape-id gradient))
(fn [] (not= (:type shape) :text))
(reset! state (:fill-color-gradient shape default-gradient)))) [:& gradient-handler-transformed
{:editing editing-spot
(mf/use-effect :from-p from-p
(mf/deps @state) :to-p to-p
(fn [] :width-p (when (= :radial (:type gradient)) width-p)
(when (not= (:fill-color-gradient shape) @state) :from-color {:value start-color :opacity start-opacity}
(st/emit! (dwc/update-shapes :to-color {:value end-color :opacity end-opacity}
[(:id shape)] :zoom zoom
#(assoc % :fill-color-gradient @state)))))) :on-change-start on-change-start
:on-change-finish on-change-finish
[:& gradient-handler-transformed :on-change-width on-change-width}])))
{: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}]))

View file

@ -28,8 +28,7 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.util.debug :refer [debug?]] [app.util.debug :refer [debug?]]
[app.main.ui.workspace.shapes.outline :refer [outline]] [app.main.ui.workspace.shapes.outline :refer [outline]]))
[app.main.ui.workspace.gradients :refer [gradient-handlers]]))
(def rotation-handler-size 25) (def rotation-handler-size 25)
(def resize-point-radius 4) (def resize-point-radius 4)
@ -210,11 +209,7 @@
(case type (case type
:rotation (when (not= :frame (:type shape)) [:> rotation-handler props]) :rotation (when (not= :frame (:type shape)) [:> rotation-handler props])
:resize-point [:> resize-point-handler props] :resize-point [:> resize-point-handler props]
:resize-side [:> resize-side-handler props]))) :resize-side [:> resize-side-handler props])))])))
#_(when (= :rect (:type shape))
[:& gradient-handlers {:shape tr-shape
:zoom zoom}])])))
;; --- Selection Handlers (Component) ;; --- Selection Handlers (Component)
(mf/defc path-edition-selection-handlers (mf/defc path-edition-selection-handlers

View file

@ -14,11 +14,11 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.shapes.filters :as filters]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [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 (defn- on-mouse-down
[event {:keys [id type] :as shape}] [event {:keys [id type] :as shape}]
@ -47,7 +47,7 @@
(st/emit! (dw/select-shape id true))) (st/emit! (dw/select-shape id true)))
(do (do
(when-not (or (empty? selected) (kbd/shift? event)) (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/select-shape id))))
(st/emit! (dw/start-move-selected))))))) (st/emit! (dw/start-move-selected)))))))
@ -70,12 +70,11 @@
#(on-mouse-down % shape)) #(on-mouse-down % shape))
on-context-menu (mf/use-callback on-context-menu (mf/use-callback
(mf/deps shape) (mf/deps shape)
#(on-context-menu % shape)) #(on-context-menu % shape))]
filter-id (mf/use-memo filters/get-filter-id)]
[:g.shape {:on-mouse-down on-mouse-down [:> shape-container {:shape shape
:on-context-menu on-context-menu :on-mouse-down on-mouse-down
:filter (filters/filter-str filter-id shape)} :on-context-menu on-context-menu}
[:& filters/filters {:filter-id filter-id :shape shape}]
[:& component {:shape shape}]]))) [:& component {:shape shape}]])))

View file

@ -19,13 +19,13 @@
[app.main.ui.workspace.shapes.common :as common] [app.main.ui.workspace.shapes.common :as common]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.filters :as filters]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.main.streams :as ms] [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? (defn- frame-wrapper-factory-equals?
[np op] [np op]
@ -99,7 +99,7 @@
(mf/deps (:id shape)) (mf/deps (:id shape))
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(st/emit! dw/deselect-all (st/emit! (dw/deselect-all)
(dw/select-shape (:id shape))))) (dw/select-shape (:id shape)))))
on-mouse-over on-mouse-over
@ -112,9 +112,7 @@
(mf/use-callback (mf/use-callback
(mf/deps (:id shape)) (mf/deps (:id shape))
(fn [] (fn []
(st/emit! (dws/change-hover-state (:id shape) false)))) (st/emit! (dws/change-hover-state (:id shape) false))))]
filter-id (mf/use-memo filters/get-filter-id)]
(when-not (:hidden shape) (when-not (:hidden shape)
[:g {:class (when selected? "selected") [:g {:class (when selected? "selected")
@ -126,8 +124,8 @@
:on-context-menu on-context-menu :on-context-menu on-context-menu
:on-double-click on-double-click :on-double-click on-double-click
:on-mouse-down on-mouse-down}] :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 [:& frame-shape
{:shape shape {:shape shape
:childs children}]]]))))) :childs children}]]])))))

View file

@ -32,7 +32,7 @@
(do (do
(dom/stop-propagation event) (dom/stop-propagation event)
(when-not (empty? selected) (when-not (empty? selected)
(st/emit! dw/deselect-all)) (st/emit! (dw/deselect-all)))
(st/emit! (dw/select-shape id)) (st/emit! (dw/select-shape id))
(st/emit! (dw/start-create-interaction)))) (st/emit! (dw/start-create-interaction))))

View file

@ -11,18 +11,19 @@
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.common.data :as d] [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.constants :as c]
[app.main.data.workspace :as dw]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.keyboard :as kbd]
[app.main.ui.shapes.path :as path] [app.main.ui.shapes.path :as path]
[app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.filters :as filters]
[app.main.ui.workspace.shapes.common :as common] [app.main.ui.shapes.shape :refer [shape-container]]
[app.main.data.workspace.drawing :as dr] [app.main.ui.workspace.shapes.common :as common]))
[app.util.dom :as dom]
[app.main.streams :as ms]
[app.util.timers :as ts]))
(mf/defc path-wrapper (mf/defc path-wrapper
{::mf/wrap-props false} {::mf/wrap-props false}
@ -42,13 +43,13 @@
(do (do
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(st/emit! (dw/start-edition-mode (:id shape))))))) (st/emit! (dw/start-edition-mode (:id shape)))))))]
filter-id (mf/use-memo filters/get-filter-id)]
[:g.shape {:on-double-click on-double-click [:> shape-container {:shape shape
:on-mouse-down on-mouse-down :on-double-click on-double-click
:on-context-menu on-context-menu :on-mouse-down on-mouse-down
:filter (filters/filter-str filter-id shape)} :on-context-menu on-context-menu}
[:& filters/filters {:filter-id filter-id :shape shape}]
[:& path/path-shape {:shape shape :background? true}]])) [:& path/path-shape {:shape shape
:background? true}]]))

View file

@ -9,32 +9,34 @@
(ns app.main.ui.workspace.shapes.text (ns app.main.ui.workspace.shapes.text
(:require (:require
[cuerdas.core :as str] ["slate" :as slate]
["slate-react" :as rslate]
[goog.events :as events] [goog.events :as events]
[goog.object :as gobj] [goog.object :as gobj]
[cuerdas.core :as str]
[rumext.alpha :as mf] [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.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 :as dw]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.texts :as dwt] [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.cursors :as cur]
[app.main.ui.workspace.shapes.common :as common] [app.main.ui.workspace.shapes.common :as common]
[app.main.ui.shapes.text :as text] [app.main.ui.shapes.text :as text]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.context :as muc] [app.main.ui.context :as muc]
[app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.filters :as filters]
[app.main.fonts :as fonts] [app.main.ui.shapes.shape :refer [shape-container]])
[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])
(:import (:import
goog.events.EventType goog.events.EventType
goog.events.KeyCodes)) goog.events.KeyCodes))
@ -78,9 +80,7 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(when selected? (when selected?
(st/emit! (dw/start-edition-mode (:id shape))))) (st/emit! (dw/start-edition-mode (:id shape)))))]
filter-id (mf/use-memo filters/get-filter-id)]
(mf/use-effect (mf/use-effect
(mf/deps shape edition selected? current-transform) (mf/deps shape edition selected? current-transform)
@ -88,26 +88,25 @@
selected? selected?
(not edition?) (not edition?)
(not embed-resources?) (not embed-resources?)
(nil? current-transform))] (nil? current-transform))
(timers/schedule #(reset! render-editor check?))))) result (timers/schedule #(reset! render-editor check?))]
#(rx/dispose! result))))
[:g.shape {:on-double-click on-double-click [:> shape-container {:shape shape
:on-mouse-down on-mouse-down :on-double-click on-double-click
:on-context-menu on-context-menu :on-mouse-down on-mouse-down
:filter (filters/filter-str filter-id shape)} :on-context-menu on-context-menu}
[:& filters/filters {:filter-id filter-id :shape shape}] (when @render-editor
[:* [:g {:opacity 0
(when @render-editor :style {:pointer-events "none"}}
[:g {:opacity 0 ;; We only render the component for its side-effect
:style {:pointer-events "none"}} [:& text-shape-edit {:shape shape
;; We only render the component for its side-effect :read-only? true}]])
[:& text-shape-edit {:shape shape
:read-only? true}]])
(if edition? (if edition?
[:& text-shape-edit {:shape shape}] [:& text-shape-edit {:shape shape}]
[:& text/text-shape {:shape shape [:& text/text-shape {:shape shape
:selected? selected?}])]])) :selected? selected?}])]))
;; --- Text Editor Rendering ;; --- Text Editor Rendering
@ -158,17 +157,25 @@
fill-color (obj/get data "fill-color" fill) fill-color (obj/get data "fill-color" fill)
fill-opacity (obj/get data "fill-opacity" opacity) 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-id (obj/get data "fill-color-ref-id")
fill-color-ref-file (obj/get data "fill-color-ref-file") fill-color-ref-file (obj/get data "fill-color-ref-file")
[r g b a] (uc/hex->rgba fill-color fill-opacity) [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) fontsdb (deref fonts/fontsdb)
base #js {:textDecoration text-decoration base #js {:textDecoration text-decoration
:color (str/format "rgba(%s, %s, %s, %s)" r g b a)
:textTransform text-transform :textTransform text-transform
:lineHeight (or line-height "inherit")}] :lineHeight (or line-height "inherit")
"--text-color" background}]
(when (and (string? letter-spacing) (when (and (string? letter-spacing)
(pos? (alength letter-spacing))) (pos? (alength letter-spacing)))
@ -243,7 +250,9 @@
childs (obj/get props "children") childs (obj/get props "children")
data (obj/get props "leaf") data (obj/get props "leaf")
style (generate-text-styles data) 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])) [:> :span attrs childs]))
(defn- render-element (defn- render-element
@ -284,6 +293,14 @@
children-count (->> node :children (map content-size) (reduce +))] children-count (->> node :children (map content-size) (reduce +))]
(+ current children-count))) (+ 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/defc text-shape-edit
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [shape read-only?] :or {read-only? false} :as props}] [{:keys [shape read-only?] :or {read-only? false} :as props}]
@ -364,7 +381,8 @@
(fn [val] (fn [val]
(when (not read-only?) (when (not read-only?)
(let [content (js->clj val :keywordize-keys true) (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 ;; 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)))})) (st/emit! (dw/update-shape id {:content (assoc content :ts (js->clj (.now js/Date)))}))
(reset! state val) (reset! state val)
@ -419,7 +437,8 @@
:x x :y y :x x :y y
:width (if (= :auto-width grow-type) 10000 width) :width (if (= :auto-width grow-type) 10000 width)
:height height} :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 [:> rslate/Slate {:editor editor
:value @state :value @state
:on-change on-change} :on-change on-change}

View file

@ -28,6 +28,7 @@
[app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.workspace.sidebar.options.typography :refer [typography-entry]] [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.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@ -190,7 +191,7 @@
:options [[(tr "workspace.assets.delete") on-delete]]}])])])) :options [[(tr "workspace.assets.delete") on-delete]]}])])]))
(mf/defc color-item (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)) (let [rename? (= (:color-for-rename @refs/workspace-local) (:id color))
id (:id color) id (:id color)
input-ref (mf/use-ref) input-ref (mf/use-ref)
@ -198,20 +199,27 @@
:top nil :top nil
:left nil :left nil
:editing rename?}) :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 click-color
(fn [event] (fn [event]
(let [ids (get-in @st/state [:workspace-local :selected])] (let [ids (get-in @st/state [:workspace-local :selected])]
(if (kbd/shift? event) (if (kbd/shift? event)
(st/emit! (dc/change-stroke ids (:value color) id (if local? nil file-id))) (st/emit! (dc/change-stroke ids color))
(st/emit! (dc/change-fill ids (:value color) id (if local? nil file-id)))))) (st/emit! (dc/change-fill ids color)))))
rename-color rename-color
(fn [name] (fn [name]
(st/emit! (dwl/update-color (assoc color :name name)))) (st/emit! (dwl/update-color (assoc color :name name))))
edit-color edit-color
(fn [value] (fn [new-color]
(st/emit! (dwl/update-color (assoc color :value value)))) (let [updated-color (merge new-color (select-keys color [:id :file-id :name]))]
(st/emit! (dwl/update-color updated-color))))
delete-color delete-color
(fn [] (fn []
@ -245,8 +253,7 @@
{:x (.-clientX event) {:x (.-clientX event)
:y (.-clientY event) :y (.-clientY event)
:on-accept edit-color :on-accept edit-color
:value (:value color) :data color
:disable-opacity true
:position :right})) :position :right}))
on-context-menu on-context-menu
@ -269,7 +276,8 @@
nil)) nil))
[:div.group-list-item {:on-context-menu on-context-menu} [: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) (if (:editing @state)
[:input.element-name [:input.element-name
{:type "text" {:type "text"
@ -278,12 +286,13 @@
:on-key-down input-key-down :on-key-down input-key-down
:auto-focus true :auto-focus true
:default-value (:name color "")}] :default-value (:name color "")}]
[:div.name-block [:div.name-block
{:on-double-click rename-color-clicked {:on-double-click rename-color-clicked
:on-click click-color} :on-click click-color}
(:name color) (:name color)
(when-not (= (:name color) (:value color)) (when-not (= (:name color) default-name)
[:span (:value color)])]) [:span default-name])])
(when local? (when local?
[:& context-menu [:& context-menu
{:selectable false {:selectable false
@ -312,8 +321,8 @@
{:x (.-clientX event) {:x (.-clientX event)
:y (.-clientY event) :y (.-clientY event)
:on-accept add-color :on-accept add-color
:value "#406280" :data {:color "#406280"
:disable-opacity true :opacity 1}
:position :right})))] :position :right})))]
[:div.asset-group [:div.asset-group
[:div.group-title {:class (when (not open?) "closed")} [:div.group-title {:class (when (not open?) "closed")}
@ -324,11 +333,14 @@
(when open? (when open?
[:div.group-list [:div.group-list
(for [color colors] (for [color colors]
[:& color-item {:key (:id color) (let [color (cond-> color
:color color (:value color) (assoc :color (:value color) :opacity 1)
:file-id file-id (:value color) (dissoc :value)
:local? local? true (assoc :file-id file-id))]
:locale locale}])])])) [:& color-item {:key (:id color)
:color color
:local? local?
:locale locale}]))])]))
(mf/defc typography-box (mf/defc typography-box
[{:keys [file file-id local? typographies locale open? on-open on-close] :as props}] [{:keys [file file-id local? typographies locale open? on-open on-close] :as props}]

View file

@ -143,10 +143,10 @@
(st/emit! (dw/select-shape id true)) (st/emit! (dw/select-shape id true))
(> (count selected) 1) (> (count selected) 1)
(st/emit! dw/deselect-all (st/emit! (dw/deselect-all)
(dw/select-shape id)) (dw/select-shape id))
:else :else
(st/emit! dw/deselect-all (st/emit! (dw/deselect-all)
(dw/select-shape id))))) (dw/select-shape id)))))
on-context-menu on-context-menu
@ -160,7 +160,7 @@
on-drag on-drag
(fn [{:keys [id]}] (fn [{:keys [id]}]
(when (not (contains? selected id)) (when (not (contains? selected id))
(st/emit! dw/deselect-all (st/emit! (dw/deselect-all)
(dw/select-shape id)))) (dw/select-shape id))))
on-drop on-drop

View file

@ -40,15 +40,15 @@
[{:keys [shape shapes-with-children page-id file-id]}] [{:keys [shape shapes-with-children page-id file-id]}]
[:* [:*
(case (:type shape) (case (:type shape)
:frame [:& frame/options {:shape shape}] :frame [:& frame/options {:shape shape}]
:group [:& group/options {:shape shape :shape-with-children shapes-with-children}] :group [:& group/options {:shape shape :shape-with-children shapes-with-children}]
:text [:& text/options {:shape shape}] :text [:& text/options {:shape shape}]
:rect [:& rect/options {:shape shape}] :rect [:& rect/options {:shape shape}]
:icon [:& icon/options {:shape shape}] :icon [:& icon/options {:shape shape}]
:circle [:& circle/options {:shape shape}] :circle [:& circle/options {:shape shape}]
:path [:& path/options {:shape shape}] :path [:& path/options {:shape shape}]
:curve [:& path/options {:shape shape}] :curve [:& path/options {:shape shape}]
:image [:& image/options {:shape shape}] :image [:& image/options {:shape shape}]
nil) nil)
[:& exports-menu [:& exports-menu
{:shape shape {:shape shape

View file

@ -21,7 +21,7 @@
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.object :as obj])) [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? (defn- fill-menu-props-equals?
[np op] [np op]
@ -36,42 +36,47 @@
(= (:fill-color new-values) (= (:fill-color new-values)
(:fill-color old-values)) (:fill-color old-values))
(= (:fill-opacity new-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/defc fill-menu
{::mf/wrap [#(mf/memo' % fill-menu-props-equals?)]} {::mf/wrap [#(mf/memo' % fill-menu-props-equals?)]}
[{:keys [ids type values editor] :as props}] [{:keys [ids type values editor] :as props}]
(let [locale (mf/deref i18n/locale) (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 label (case type
:multiple (t locale "workspace.options.selection-fill") :multiple (t locale "workspace.options.selection-fill")
:group (t locale "workspace.options.group-fill") :group (t locale "workspace.options.group-fill")
(t locale "workspace.options.fill")) (t locale "workspace.options.fill"))
color {:value (:fill-color values) color {:color (:fill-color values)
:opacity (:fill-opacity values) :opacity (:fill-opacity values)
:id (:fill-color-ref-id 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 on-add
(mf/use-callback (mf/use-callback
(mf/deps ids) (mf/deps ids)
(fn [event] (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 on-delete
(mf/use-callback (mf/use-callback
(mf/deps ids) (mf/deps ids)
(fn [event] (fn [event]
(st/emit! (dc/change-fill ids nil nil nil)))) (st/emit! (dc/change-fill ids nil))))
on-change on-change
(mf/use-callback (mf/use-callback
(mf/deps ids) (mf/deps ids)
(fn [value opacity id file-id] (fn [color]
(st/emit! (dc/change-fill ids value opacity id file-id)))) (st/emit! (dc/change-fill ids color))))
on-open-picker on-open-picker
(mf/use-callback (mf/use-callback

View file

@ -101,14 +101,17 @@
(assoc-in [:params :item-length] item-length))))) (assoc-in [:params :item-length] item-length)))))
handle-change-color handle-change-color
(fn [value opacity] (fn [color]
(emit-changes! #(-> % (emit-changes! #(-> % (assoc-in [:params :color] color))))
(assoc-in [:params :color :value] value)
(assoc-in [:params :color :opacity] opacity))))
handle-use-default handle-use-default
(fn [] (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 handle-set-as-default
(fn [] (fn []
@ -214,6 +217,7 @@
:on-change (handle-change :params :margin)}]]) :on-change (handle-change :params :margin)}]])
[:& color-row {:color (:color params) [:& color-row {:color (:color params)
:disable-gradient true
:on-change handle-change-color}] :on-change handle-change-color}]
[:div.row-flex [:div.row-flex
[:button.btn-options {:disabled is-default [:button.btn-options {:disabled is-default

View file

@ -44,8 +44,9 @@
[:div.element-set [:div.element-set
[:div.element-set-title (t locale "workspace.options.canvas-background")] [:div.element-set-title (t locale "workspace.options.canvas-background")]
[:div.element-set-content [:div.element-set-content
[:& color-row {:disable-opacity true [:& color-row {:disable-gradient true
:color {:value (get options :background "#E8E9EA") :disable-opacity true
:color {:color (get options :background "#E8E9EA")
:opacity 1} :opacity 1}
:on-change handle-change-color :on-change handle-change-color
:on-open on-open :on-open on-open

View file

@ -10,31 +10,33 @@
(ns app.main.ui.workspace.sidebar.options.rows.color-row (ns app.main.ui.workspace.sidebar.options.rows.color-row
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str]
[app.common.math :as math] [app.common.math :as math]
[app.common.pages :as cp]
[app.common.data :as d]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.data :refer [classnames]] [app.util.data :refer [classnames]]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.color :as uc]
[app.main.refs :as refs]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.common.data :as d] [app.main.ui.components.color-bullet :as cb]))
[app.main.refs :as refs]))
(defn color-picker-callback (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] (fn [event]
(let [x (.-clientX event) (let [x (.-clientX event)
y (.-clientY event) y (.-clientY event)
props {:x x props {:x x
:y y :y y
:disable-gradient disable-gradient
:disable-opacity disable-opacity
:on-change handle-change-color :on-change handle-change-color
:on-close handle-close :on-close handle-close
:value (:value color) :data color}]
:opacity (:opacity color)
:disable-opacity disable-opacity}]
(handle-open) (handle-open)
(modal/show! :colorpicker props)))) (modal/show! :colorpicker props))))
(defn value-to-background [value]
(if (= value :multiple) "transparent" value))
(defn remove-hash [value] (defn remove-hash [value]
(if (or (nil? value) (= value :multiple)) "" (subs value 1))) (if (or (nil? value) (= value :multiple)) "" (subs value 1)))
@ -59,38 +61,28 @@
(if (= v :multiple) nil v)) (if (= v :multiple) nil v))
(mf/defc color-row (mf/defc color-row
[{:keys [color on-change on-open on-close disable-opacity]}] [{:keys [color disable-gradient disable-opacity on-change on-open on-close]}]
(let [;; (let [file-colors (mf/deref refs/workspace-file-colors)
file-colors (mf/deref refs/workspace-file-colors)
shared-libs (mf/deref refs/workspace-libraries) shared-libs (mf/deref refs/workspace-libraries)
get-color-name (fn [{:keys [id file-id]}] get-color-name (fn [{:keys [id file-id]}]
(let [src-colors (if file-id (get-in shared-libs [file-id :data :colors]) file-colors)] (let [src-colors (if file-id (get-in shared-libs [file-id :data :colors]) file-colors)]
(get-in src-colors [id :name]))) (get-in src-colors [id :name])))
default-color {:value "#000000" :opacity 1}
parse-color (fn [color] parse-color (fn [color]
(-> (merge default-color color) (-> color
(update :value #(or % "#000000")) (update :color #(or % (:value color)))))
(update :opacity #(or % 1))))
state (mf/use-state (parse-color color))
value (:value @state)
opacity (:opacity @state)
change-value (fn [new-value] change-value (fn [new-value]
(swap! state assoc :value new-value) (when on-change (on-change (-> color
(when on-change (on-change new-value (remove-multiple opacity)))) (assoc :color new-value)
(dissoc :gradient)))))
change-opacity (fn [new-opacity] change-opacity (fn [new-opacity]
(swap! state assoc :opacity new-opacity) (when on-change (on-change (assoc color :opacity new-opacity))))
(when on-change (on-change (remove-multiple value) new-opacity)))
handle-pick-color (fn [new-value new-opacity id file-id] handle-pick-color (fn [color]
(reset! state {:value new-value :opacity new-opacity}) (when on-change (on-change color)))
(when on-change (on-change new-value new-opacity id file-id)))
handle-open (fn [] (when on-open (on-open))) handle-open (fn [] (when on-open (on-open)))
@ -114,37 +106,63 @@
change-opacity)))) change-opacity))))
select-all (fn [event] 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/use-effect
(mf/deps color) (mf/deps color)
#(reset! state (parse-color color))) (fn []
(modal/update-props! :colorpicker {:data (parse-color color)})))
[:div.row-flex.color-data [:div.row-flex.color-data
[:span.color-th [:& cb/color-bullet {:color color
{:class (when (and (:id color) (not= (:id color) :multiple)) "color-name") :on-click handle-click-color}]
: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) "?")]
(if (:id color) (cond
;; Rendering a color with ID
(:id color)
[:div.color-info [:div.color-info
[:div.color-name (str (get-color-name color))]] [: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 [:div.color-info
[:input {:value (-> value remove-hash) [:div.color-name (cb/gradient-type->string (get-in color [:gradient :type]))]]
:pattern "^[0-9a-fA-F]{0,6}$"
:placeholder (tr "settings.multiple")
:on-click select-all
:on-change handle-value-change}]])
(when (not disable-opacity) ;; Rendering a plain color/opacity
[:div.input-element :else
{:class (classnames :percentail (not= opacity :multiple))} [:*
[:input.input-text {:type "number" [:div.color-info
:value (-> opacity opacity->string) [:input {:value (if (uc/multiple? color)
:placeholder (tr "settings.multiple") ""
:on-click select-all (-> color :color remove-hash))
:on-change handle-opacity-change :pattern "^[0-9a-fA-F]{0,6}$"
:min "0" :placeholder (tr "settings.multiple")
:max "100"}]])])) :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"}]])])]))

View file

@ -174,7 +174,11 @@
[:span.after (t locale "workspace.options.shadow-options.spread")]]] [:span.after (t locale "workspace.options.shadow-options.spread")]]]
[:div.color-row-wrap [: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-change (update-color index)
:on-open #(st/emit! dwc/start-undo-transaction) :on-open #(st/emit! dwc/start-undo-transaction)
:on-close #(st/emit! dwc/commit-undo-transaction)}]]]])) :on-close #(st/emit! dwc/commit-undo-transaction)}]]]]))

View file

@ -14,6 +14,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.math :as math] [app.common.math :as math]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.colors :as dc]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
@ -29,7 +30,8 @@
:stroke-color :stroke-color
:stroke-color-ref-id :stroke-color-ref-id
:stroke-color-ref-file :stroke-color-ref-file
:stroke-opacity]) :stroke-opacity
:stroke-color-gradient])
(defn- stroke-menu-props-equals? (defn- stroke-menu-props-equals?
[np op] [np op]
@ -72,19 +74,17 @@
show-options (not= (:stroke-style values :none) :none) 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) :opacity (:stroke-opacity values)
:id (:stroke-color-ref-id 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 handle-change-stroke-color
(fn [value opacity id file-id] (mf/use-callback
(let [change #(cond-> % (mf/deps ids)
value (assoc :stroke-color value (fn [color]
:stroke-color-ref-id id (st/emit! (dc/change-stroke ids color))))
:stroke-color-ref-file file-id)
opacity (assoc :stroke-opacity opacity))]
(st/emit! (dwc/update-shapes ids change))))
on-stroke-style-change on-stroke-style-change
(fn [event] (fn [event]

View file

@ -32,7 +32,7 @@
["slate" :refer [Transforms]])) ["slate" :refer [Transforms]]))
(def text-typography-attrs [:typography-ref-id :typography-ref-file]) (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-font-attrs [:font-id :font-family :font-variant-id :font-size :font-weight :font-style])
(def text-align-attrs [:text-align]) (def text-align-attrs [:text-align])
(def text-spacing-attrs [:line-height :letter-spacing]) (def text-spacing-attrs [:line-height :letter-spacing])
@ -291,6 +291,8 @@
:shape shape :shape shape
:attrs text-fill-attrs}) :attrs text-fill-attrs})
fill-values (d/update-in-when fill-values [:fill-color-gradient :type] keyword)
fill-values (cond-> fill-values fill-values (cond-> fill-values
;; Keep for backwards compatibility ;; Keep for backwards compatibility
(:fill fill-values) (assoc :fill-color (:fill fill-values)) (:fill fill-values) (assoc :fill-color (:fill fill-values))

View file

@ -40,6 +40,8 @@
[app.main.ui.workspace.snap-distances :refer [snap-distances]] [app.main.ui.workspace.snap-distances :refer [snap-distances]]
[app.main.ui.workspace.frame-grid :refer [frame-grid]] [app.main.ui.workspace.frame-grid :refer [frame-grid]]
[app.main.ui.workspace.shapes.outline :refer [outline]] [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.common.math :as mth]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.dom.dnd :as dnd] [app.util.dom.dnd :as dnd]
@ -184,104 +186,6 @@
(:width vbox 0) (:width vbox 0)
(:height 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 (mf/defc viewport
[{:keys [page-id page local layout] :as props}] [{:keys [page-id page local layout] :as props}]
(let [{:keys [options-mode (let [{:keys [options-mode
@ -309,8 +213,6 @@
drawing-tool (:tool drawing) drawing-tool (:tool drawing)
drawing-obj (:object drawing) drawing-obj (:object drawing)
pick-color (mf/use-state [255 255 255 255])
zoom (or zoom 1) zoom (or zoom 1)
on-mouse-down on-mouse-down
@ -586,11 +488,11 @@
[:* [:*
(when picking-color? (when picking-color?
[:& pixel-picker-overlay {:vport vport [:& pixel-overlay {:vport vport
:vbox vbox :vbox vbox
:viewport-ref viewport-ref :viewport-ref viewport-ref
:options options :options options
:layout layout}]) :layout layout}])
[:svg.viewport [:svg.viewport
{:preserveAspectRatio "xMidYMid meet" {:preserveAspectRatio "xMidYMid meet"
@ -642,6 +544,10 @@
:zoom zoom :zoom zoom
:edition edition}]) :edition edition}])
(when (= (count selected) 1)
[:& gradient-handlers {:id (first selected)
:zoom zoom}])
(when drawing-obj (when drawing-obj
[:& draw-area {:shape drawing-obj [:& draw-area {:shape drawing-obj
:zoom zoom :zoom zoom

View file

@ -77,3 +77,35 @@
(defn hsv->hsl (defn hsv->hsl
[hsv] [hsv]
(hex->hsl (hsv->hex 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)))

View file

@ -15,6 +15,8 @@
[goog.object :as gobj] [goog.object :as gobj]
["lodash/omit" :as omit])) ["lodash/omit" :as omit]))
(defn new [] #js {})
(defn get (defn get
([obj k] ([obj k]
(when-not (nil? obj) (when-not (nil? obj)