mirror of
https://github.com/penpot/penpot.git
synced 2025-04-15 08:21:40 -05:00
Merge pull request #353 from uxbox/560/gradients
Gradients infrastructure, color picker with harmony and HSV sliders
This commit is contained in:
commit
3f627cb132
14 changed files with 1388 additions and 472 deletions
|
@ -199,3 +199,27 @@
|
|||
(defn center-points [points]
|
||||
(let [k (point (count points))]
|
||||
(reduce #(add %1 (divide %2 k)) (point) points)))
|
||||
|
||||
(defn normal-left
|
||||
"Returns the normal unit vector on the left side"
|
||||
[{:keys [x y]}]
|
||||
(unit (point (- y) x)))
|
||||
|
||||
(defn normal-right
|
||||
"Returns the normal unit vector on the right side"
|
||||
[{:keys [x y]}]
|
||||
(unit (point y (- x))))
|
||||
|
||||
(defn point-line-distance
|
||||
"Returns the distance from a point to a line defined by two points"
|
||||
[point line-point1 line-point2]
|
||||
(let [{x0 :x y0 :y} point
|
||||
{x1 :x y1 :y} line-point1
|
||||
{x2 :x y2 :y} line-point2
|
||||
num (mth/abs
|
||||
(+ (* x0 (- y2 y1))
|
||||
(- (* y0 (- x2 x1)))
|
||||
(* x2 y1)
|
||||
(- (* y2 x1))))
|
||||
dist (distance line-point2 line-point1)]
|
||||
(/ num dist)))
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
#?(:cljs
|
||||
(:require [goog.math :as math])))
|
||||
|
||||
(def PI
|
||||
#?(:cljs (.-PI js/Math)
|
||||
:clj Math/PI))
|
||||
|
||||
(defn nan?
|
||||
[v]
|
||||
#?(:cljs (js/isNaN v)
|
||||
|
|
2
frontend/resources/images/icons/picker-harmony.svg
Normal file
2
frontend/resources/images/icons/picker-harmony.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" height="500" width="500"><path d="M330.902 0c-86.375 0-156.677 70.242-156.677 156.617 0 86.376 70.302 156.676 156.677 156.676 86.375 0 156.616-70.3 156.616-156.676C487.518 70.242 417.278 0 330.902 0zm-.562 36.625a120.87 120.87 0 01.342 0 120.87 120.87 0 01120.87 120.87 120.87 120.87 0 01-120.87 120.87 120.87 120.87 0 01-120.87-120.87A120.87 120.87 0 01330.34 36.624zM126.557 271.852c-62.798 0-114.075 51.275-114.075 114.074C12.482 448.724 63.76 500 126.557 500S240.63 448.724 240.63 385.926c0-62.799-51.276-114.074-114.074-114.074zm-.268 38.396a76.236 76.236 0 01.074 0 76.236 76.236 0 0176.237 76.236 76.236 76.236 0 01-76.237 76.237 76.236 76.236 0 01-76.236-76.237 76.236 76.236 0 0176.162-76.236z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 775 B |
1
frontend/resources/images/icons/picker-hsv.svg
Normal file
1
frontend/resources/images/icons/picker-hsv.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" height="500" width="500"><path d="M164.617 17.078c-49.266 0-90.208 37.515-95.681 85.381h-49.47A19.467 19.467 0 000 121.928a19.467 19.467 0 0019.467 19.467h53.025c12.073 39.418 48.894 68.306 92.125 68.306 43.23 0 80.054-28.888 92.127-68.306h223.79A19.467 19.467 0 00500 121.928a19.467 19.467 0 00-19.467-19.469H260.301c-5.474-47.866-46.418-85.38-95.684-85.38zm-.787 37.004a60.924 60.924 0 0160.924 60.924 60.924 60.924 0 01-60.924 60.924 60.924 60.924 0 01-60.924-60.924 60.924 60.924 0 0160.924-60.924zm171.553 236.217c-49.267 0-90.21 37.518-95.684 85.385H19.467A19.467 19.467 0 000 395.148a19.467 19.467 0 0019.467 19.467h223.789c12.073 39.42 48.896 68.307 92.127 68.307 43.23 0 80.052-28.888 92.125-68.307h53.025A19.467 19.467 0 00500 395.148a19.467 19.467 0 00-19.467-19.464h-49.469c-5.472-47.867-46.414-85.385-95.681-85.385zm1.119 36.148a60.924 60.924 0 0160.924 60.924 60.924 60.924 0 01-60.924 60.924 60.924 60.924 0 01-60.924-60.924 60.924 60.924 0 0160.924-60.924z"/></svg>
|
After Width: | Height: | Size: 1 KiB |
1
frontend/resources/images/icons/picker-ramp.svg
Normal file
1
frontend/resources/images/icons/picker-ramp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 332 B |
|
@ -1 +1,4 @@
|
|||
<svg height="500" viewBox="0 0 500.00001 500.00001" width="500" xmlns="http://www.w3.org/2000/svg"><path d="m11.402 498.536c-4.64-1.613-8.317-5.335-9.896-10.02-.732-2.168-.824-7.17-.824-44.7 0-45.853-.04-45.148 2.807-49.542.653-1.01 53.238-53.866 116.855-117.46 63.617-63.59 115.667-115.852 115.667-116.134s-11.486-11.997-25.524-26.033-25.524-25.75-25.524-26.034c0-.283 14.816-15.328 32.924-33.434l32.924-32.922 29.607 29.59 29.607 29.592 44.286-44.17c32.81-32.726 45.278-44.843 48.115-46.76 7.434-5.025 16.28-8.752 24.758-10.432 5.316-1.053 18.308-.91 23.737.26 25.043 5.4 44.058 24.808 49.064 50.08.927 4.68.927 17.78 0 22.46-1.68 8.48-5.407 17.326-10.432 24.76-1.917 2.836-14.034 15.303-46.76 48.114l-44.17 44.286 29.59 29.608 29.592 29.608-32.92 32.923c-18.107 18.108-33.152 32.924-33.435 32.924s-11.998-11.485-26.034-25.523-25.75-25.524-26.033-25.524c-.282 0-52.543 52.05-116.135 115.667-63.593 63.617-116.45 116.202-117.46 116.856-4.4 2.85-3.662 2.81-49.686 2.783-37.102-.023-42.692-.126-44.702-.824zm188.236-144.766c62.962-62.96 114.477-114.7 114.477-114.98 0-.67-52.165-52.838-52.834-52.838-.282 0-52.026 51.515-114.986 114.477l-114.475 114.475v26.057c0 19.588.153 26.21.614 26.67.46.462 7.084.614 26.67.614h26.056l114.48-114.475z"/></svg>
|
||||
<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"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -6,257 +6,487 @@
|
|||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
.colorpicker {
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
.colorpicker-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
|
||||
& > * {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
background-color: $color-white;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
margin-bottom: 0.25rem;
|
||||
justify-content: space-between;
|
||||
|
||||
& > * {
|
||||
width: 200px;
|
||||
.picker-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:hover svg {
|
||||
fill: $color-primary;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gradients-buttons {
|
||||
.gradient {
|
||||
cursor: pointer;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid $color-gray-20;
|
||||
border-radius: 2px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
display: flex;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
.picker-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:hover svg {
|
||||
fill: $color-primary;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
.active {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
|
||||
.picker-detail-wrapper {
|
||||
position: relative;
|
||||
.linear-gradient {
|
||||
background: linear-gradient(180deg, $color-gray-20, transparent);
|
||||
}
|
||||
|
||||
.center-circle {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid $color-white;
|
||||
border-radius: 8px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-7px, -7px);
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
.radial-gradient {
|
||||
background: radial-gradient(transparent, $color-gray-20);
|
||||
}
|
||||
#picker-detail {
|
||||
border: 1px solid $color-gray-10;
|
||||
}
|
||||
|
||||
.gradient-stops {
|
||||
height: 10px;
|
||||
display: flex;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.gradient-background {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid $color-gray-10;
|
||||
}
|
||||
|
||||
.gradient-stop-wrapper {
|
||||
position: absolute;
|
||||
width: calc(100% - 2rem);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.gradient-stop {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid $color-gray-20;
|
||||
margin-top: -2px;
|
||||
margin-left: -7px;
|
||||
box-shadow: 0 2px 2px rgb(0 0 0 / 15%);
|
||||
|
||||
.selected {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.picker-detail-wrapper {
|
||||
position: relative;
|
||||
|
||||
.center-circle {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid $color-white;
|
||||
border-radius: 8px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-7px, -7px);
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
}
|
||||
|
||||
#picker-detail {
|
||||
border: 1px solid $color-gray-10;
|
||||
}
|
||||
|
||||
.slider-selector {
|
||||
--gradient-direction: 90deg;
|
||||
--background-repeat: left;
|
||||
|
||||
&.vertical {
|
||||
--gradient-direction: 0deg;
|
||||
--background-repeat: top;
|
||||
}
|
||||
|
||||
border: 1px solid $color-gray-10;
|
||||
|
||||
background: linear-gradient(var(--gradient-direction), rgba(var(--color), 0) 0%, rgba(var(--color), 1.0) 100%);
|
||||
align-self: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
width: 100%;
|
||||
height: calc(0.5rem + 1px);
|
||||
|
||||
&.vertical {
|
||||
width: calc(0.5rem + 1px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.hue {
|
||||
background: linear-gradient(
|
||||
var(--gradient-direction),
|
||||
#f00 0%, #ff0 17%, #0f0 33%, #0ff 50%,
|
||||
#00f 67%, #f0f 83%, #f00 100%);
|
||||
}
|
||||
|
||||
&.saturation {
|
||||
background: linear-gradient(
|
||||
var(--gradient-direction),
|
||||
var(--saturation-grad-from) 0%,
|
||||
var(--saturation-grad-to) 100%
|
||||
)
|
||||
}
|
||||
|
||||
&.opacity {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") var(--background-repeat) center;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(var(--gradient-direction), rgba(var(--color), 0) 0%, rgba(var(--color), 1.0) 100%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.value {
|
||||
background: linear-gradient(var(--gradient-direction), #FFF 0%, #000 100%);
|
||||
}
|
||||
|
||||
|
||||
.handler {
|
||||
background-color: $color-white;;
|
||||
box-shadow: rgba(0, 0, 0, 0.37) 0px 1px 4px 0px;
|
||||
transform: translate(-6px, -2px);
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.vertical .handler {
|
||||
transform: translate(-6px, 6px);
|
||||
}
|
||||
}
|
||||
|
||||
.value-saturation-selector {
|
||||
background-color: rgba(var(--hue-rgb));
|
||||
position: relative;
|
||||
height: 6.75rem;
|
||||
cursor: pointer;
|
||||
|
||||
.handler {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
z-index: 1;
|
||||
border: 1px solid $color-white;
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset, rgb(0 0 0 / 0.25) 0px 4px 4px inset, rgb(0 0 0 / 0.25) 0px 4px 4px;
|
||||
transform: translate(-6px, -6px);
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #fff, rgba(255,255,255,0));
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to top, #000, rgba(0,0,0,0));
|
||||
}
|
||||
}
|
||||
|
||||
.color-bullet {
|
||||
grid-area: color;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: rgba(var(--color));
|
||||
border-radius: 12px;
|
||||
border: 1px solid $color-gray-10;
|
||||
}
|
||||
|
||||
.shade-selector {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
grid-template-areas: "color hue"
|
||||
"color opacity";
|
||||
grid-template-columns: 2.5rem 1fr;
|
||||
height: 3.5rem;
|
||||
grid-row-gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
.slider-selector.hue {
|
||||
grid-area: "hue";
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.slider-selector.opacity {
|
||||
grid-area: "opacity";
|
||||
align-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
.color-values {
|
||||
display: grid;
|
||||
grid-template-columns: 3.5rem repeat(4, 1fr);
|
||||
grid-row-gap: 0.25rem;
|
||||
justify-items: center;
|
||||
grid-column-gap: 0.25rem;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: 1px solid $color-gray-10;
|
||||
border-radius: 2px;
|
||||
font-size: $fs11;
|
||||
height: 1.5rem;
|
||||
padding: 0 $x-small;
|
||||
color: $color-gray-40;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: $fs11;
|
||||
}
|
||||
}
|
||||
|
||||
.libraries {
|
||||
border-top: 1px solid $color-gray-10;
|
||||
padding-top: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
width: 200px;
|
||||
|
||||
select {
|
||||
background-image: url(/images/icons/arrow-down.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 95% 48%;
|
||||
background-size: 10px;
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 2px 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: $color-gray-40;
|
||||
border-color: $color-gray-10;
|
||||
border-radius: 2px;
|
||||
|
||||
option {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-colors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
justify-content: space-between;
|
||||
margin-right: -8px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 5.5rem;
|
||||
}
|
||||
|
||||
|
||||
.selected-colors::after {
|
||||
content: "";
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.selected-colors .color-bullet {
|
||||
grid-area: auto;
|
||||
margin-bottom: 0.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
|
||||
&.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.button svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: $color-gray-30;
|
||||
}
|
||||
|
||||
&.plus-button svg {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
fill: $color-black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
.btn-primary {
|
||||
height: 1.5rem;
|
||||
padding: 0 2.5rem;
|
||||
font-size: $fs12;
|
||||
}
|
||||
}
|
||||
|
||||
.harmony-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.hue-wheel-wrapper {
|
||||
position: relative;
|
||||
|
||||
.hue-wheel {
|
||||
width: 152px;
|
||||
height: 152px;
|
||||
}
|
||||
|
||||
.handler {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.value-selector {
|
||||
background-color: rgba(var(--hue));
|
||||
position: relative;
|
||||
height: 6.75rem;
|
||||
cursor: pointer;
|
||||
|
||||
.handler {
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset;
|
||||
transform: translate(-6px, -6px);
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.value-selector::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #fff, rgba(255,255,255,0));
|
||||
}
|
||||
|
||||
.value-selector::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to top, #000, rgba(0,0,0,0));
|
||||
}
|
||||
|
||||
.shade-selector {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
grid-template-areas: "color hue" "color opacity";
|
||||
grid-template-columns: 2.5rem 1fr;
|
||||
height: 3.5rem;
|
||||
grid-row-gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-bullet {
|
||||
grid-area: color;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: rgba(var(--color));
|
||||
border-radius: 12px;
|
||||
border: 1px solid $color-gray-10;
|
||||
}
|
||||
|
||||
.hue-selector {
|
||||
align-self: end;
|
||||
grid-area: hue;
|
||||
height: 0.5rem;
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#f00 0%, #ff0 17%, #0f0 33%, #0ff 50%,
|
||||
#00f 67%, #f0f 83%, #f00 100%);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hue-selector .handler,
|
||||
.opacity-selector .handler {
|
||||
background-color: rgb(248, 248, 248);
|
||||
box-shadow: rgba(0, 0, 0, 0.37) 0px 1px 4px 0px;
|
||||
transform: translate(-6px, -2px);
|
||||
border: 1px solid $color-white;
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset, rgb(0 0 0 / 0.25) 0px 4px 4px inset, rgb(0 0 0 / 0.25) 0px 4px 4px;
|
||||
transform: translate(-6px, -6px);
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.handler.complement {
|
||||
background-color: $color-white;
|
||||
box-shadow: rgb(0 0 0 / 0.25) 0px 4px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.opacity-selector {
|
||||
align-self: start;
|
||||
grid-area: opacity;
|
||||
height: 0.5rem;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") left center;
|
||||
}
|
||||
|
||||
.opacity-selector::after {
|
||||
content: "";
|
||||
background: linear-gradient(to right, rgba(var(--color), 0) 0%, rgba(var(--color), 1.0) 100%);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
.handlers-wrapper {
|
||||
height: 152px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
justify-content: space-around;
|
||||
padding-top: 0.5rem;
|
||||
|
||||
& > * {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hsva-selector {
|
||||
display: grid;
|
||||
padding: 0.25rem;
|
||||
grid-template-columns: 20px 1fr;
|
||||
grid-template-rows: repeat(4, 2rem);
|
||||
grid-row-gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.hue,
|
||||
.saturation,
|
||||
.value,
|
||||
.opacity {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.color-values {
|
||||
display: grid;
|
||||
grid-template-columns: 3.5rem repeat(4, 1fr);
|
||||
grid-row-gap: 0.25rem;
|
||||
justify-items: center;
|
||||
grid-column-gap: 0.25rem;
|
||||
.hsva-selector-label {
|
||||
grid-column: 1;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: 1px solid $color-gray-10;
|
||||
border-radius: 2px;
|
||||
font-size: $fs11;
|
||||
height: 1.5rem;
|
||||
padding: 0 $x-small;
|
||||
color: $color-gray-40;
|
||||
}
|
||||
.colorpicker-tooltip {
|
||||
border-radius: $br-small;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 1400px;
|
||||
top: 100px;
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
width: auto;
|
||||
|
||||
label {
|
||||
font-size: $fs11;
|
||||
}
|
||||
span {
|
||||
color: $color-gray-20;
|
||||
font-size: $fs12;
|
||||
}
|
||||
|
||||
.inputs-area {
|
||||
|
||||
.input-text {
|
||||
color: $color-gray-60;
|
||||
font-size: $fs13;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.libraries {
|
||||
border-top: 1px solid $color-gray-10;
|
||||
padding-top: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
width: 200px;
|
||||
|
||||
select {
|
||||
background-image: url(/images/icons/arrow-down.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 95% 48%;
|
||||
background-size: 10px;
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 2px 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: $color-gray-40;
|
||||
border-color: $color-gray-10;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
option {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.colorpicker-tabs {
|
||||
display: flex;
|
||||
margin-top: 0.25rem;
|
||||
height: 2rem;
|
||||
background-color: $color-gray-10;
|
||||
|
||||
.selected-colors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
justify-content: space-between;
|
||||
margin-right: -8px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 5.5rem;
|
||||
}
|
||||
|
||||
|
||||
.selected-colors::after {
|
||||
content: "";
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.selected-colors .color-bullet {
|
||||
grid-area: auto;
|
||||
margin-bottom: 0.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
|
||||
&.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.button svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: $color-gray-30;
|
||||
}
|
||||
|
||||
&.plus-button svg {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
fill: $color-black;
|
||||
}
|
||||
}
|
||||
.active {
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
.colorpicker-tab {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.btn-primary {
|
||||
height: 1.5rem;
|
||||
padding: 0 2.5rem;
|
||||
font-size: $fs12;
|
||||
}
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: $color-gray-30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-data {
|
||||
|
@ -265,8 +495,8 @@
|
|||
position: relative;
|
||||
|
||||
.color-name {
|
||||
font-size: $fs13;
|
||||
margin: 5px 6px 0px 6px;
|
||||
font-size: $fs13;
|
||||
margin: 5px 6px 0px 6px;
|
||||
}
|
||||
|
||||
.color-info {
|
||||
|
@ -310,30 +540,3 @@
|
|||
}
|
||||
}
|
||||
|
||||
.colorpicker-tooltip {
|
||||
border-radius: $br-small;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 1400px;
|
||||
top: 100px;
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
width: auto;
|
||||
|
||||
span {
|
||||
color: $color-gray-20;
|
||||
font-size: $fs12;
|
||||
}
|
||||
|
||||
.inputs-area {
|
||||
|
||||
.input-text {
|
||||
color: $color-gray-60;
|
||||
font-size: $fs13;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,6 +122,9 @@
|
|||
(def uppercase (icon-xref :uppercase))
|
||||
(def user (icon-xref :user))
|
||||
(def tick (icon-xref :tick))
|
||||
(def picker-harmony (icon-xref :picker-harmony))
|
||||
(def picker-hsv (icon-xref :picker-hsv))
|
||||
(def picker-ramp (icon-xref :picker-ramp))
|
||||
|
||||
(def loader-pencil
|
||||
(mf/html
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
;; Copyright (c) 2016-2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.shapes.attrs
|
||||
(:require [app.util.object :as obj]))
|
||||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[app.util.object :as obj]))
|
||||
|
||||
(defn- stroke-type->dasharray
|
||||
[style]
|
||||
|
@ -19,16 +21,20 @@
|
|||
nil))
|
||||
|
||||
(defn extract-style-attrs
|
||||
[shape]
|
||||
(let [stroke-style (:stroke-style shape :none)
|
||||
attrs #js {:fill (or (:fill-color shape) "transparent")
|
||||
:fillOpacity (:fill-opacity shape nil)
|
||||
:rx (:rx shape nil)
|
||||
:ry (:ry shape nil)}]
|
||||
(when (not= stroke-style :none)
|
||||
(obj/merge! attrs
|
||||
#js {:stroke (:stroke-color shape nil)
|
||||
:strokeWidth (:stroke-width shape 1)
|
||||
:strokeOpacity (:stroke-opacity shape nil)
|
||||
:strokeDasharray (stroke-type->dasharray stroke-style)}))
|
||||
attrs))
|
||||
([shape] (extract-style-attrs shape nil))
|
||||
([shape gradient-id]
|
||||
(let [stroke-style (:stroke-style shape :none)
|
||||
attrs #js {:rx (:rx shape nil)
|
||||
:ry (:ry shape nil)}
|
||||
attrs (obj/merge! attrs
|
||||
(if gradient-id
|
||||
#js {:fill (str/format "url(#%s)" gradient-id)}
|
||||
#js {:fill (or (:fill-color shape) "transparent")
|
||||
:fillOpacity (:fill-opacity shape nil)}))]
|
||||
(when (not= stroke-style :none)
|
||||
(obj/merge! attrs
|
||||
#js {:stroke (:stroke-color shape nil)
|
||||
:strokeWidth (:stroke-width shape 1)
|
||||
:strokeOpacity (:stroke-opacity shape nil)
|
||||
:strokeDasharray (stroke-type->dasharray stroke-style)}))
|
||||
attrs)))
|
||||
|
|
78
frontend/src/app/main/ui/shapes/gradients.cljs
Normal file
78
frontend/src/app/main/ui/shapes/gradients.cljs
Normal file
|
@ -0,0 +1,78 @@
|
|||
;; 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.shapes.gradients
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
[goog.object :as gobj]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.geom.point :as gpt]))
|
||||
|
||||
(mf/defc linear-gradient [{:keys [id shape gradient]}]
|
||||
(let [{:keys [x y width height]} shape]
|
||||
[:defs
|
||||
[:linearGradient {:id id
|
||||
:x1 (:start-x gradient)
|
||||
:y1 (:start-y gradient)
|
||||
:x2 (:end-x gradient)
|
||||
:y2 (:end-y gradient)}
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
:offset (or offset 0)
|
||||
:stop-color color
|
||||
:stop-opacity opacity}])]]))
|
||||
|
||||
(mf/defc radial-gradient [{:keys [id shape gradient]}]
|
||||
(let [{:keys [x y width height]} shape]
|
||||
[:defs
|
||||
(let [translate-vec (gpt/point (+ x (* width (:start-x gradient)))
|
||||
(+ y (* height (:start-y gradient))))
|
||||
|
||||
|
||||
gradient-vec (gpt/to-vec (gpt/point (* width (:start-x gradient))
|
||||
(* height (:start-y gradient)))
|
||||
(gpt/point (* width (:end-x gradient))
|
||||
(* height (:end-y gradient))))
|
||||
|
||||
angle (gpt/angle gradient-vec
|
||||
(gpt/point 1 0))
|
||||
|
||||
shape-height-vec (gpt/point 0 (/ height 2))
|
||||
|
||||
scale-factor-y (/ (gpt/length gradient-vec) (/ height 2))
|
||||
scale-factor-x (* scale-factor-y (:width gradient))
|
||||
|
||||
scale-vec (gpt/point (* scale-factor-y (/ height 2))
|
||||
(* scale-factor-x (/ width 2))
|
||||
)
|
||||
tr-translate (str/fmt "translate(%s, %s)" (:x translate-vec) (:y translate-vec))
|
||||
tr-rotate (str/fmt "rotate(%s)" angle)
|
||||
tr-scale (str/fmt "scale(%s, %s)" (:x scale-vec) (:y scale-vec))
|
||||
transform (str/fmt "%s %s %s" tr-translate tr-rotate tr-scale)]
|
||||
[:radialGradient {:id id
|
||||
:cx 0
|
||||
:cy 0
|
||||
:r 1
|
||||
:gradientUnits "userSpaceOnUse"
|
||||
:gradientTransform transform}
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
:offset (or offset 0)
|
||||
:stop-color color
|
||||
:stop-opacity opacity}])])]))
|
||||
|
||||
(mf/defc gradient
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [gradient (gobj/get props "gradient")]
|
||||
(case (:type gradient)
|
||||
:linear [:> linear-gradient props]
|
||||
:radial [:> radial-gradient props]
|
||||
nil)))
|
|
@ -13,7 +13,12 @@
|
|||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.util.object :as obj]))
|
||||
[app.util.object :as obj]
|
||||
[app.main.ui.shapes.gradients :refer [gradient]]
|
||||
|
||||
[cuerdas.core :as str]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.geom.point :as gpt]))
|
||||
|
||||
(mf/defc rect-shape
|
||||
{::mf/wrap-props false}
|
||||
|
@ -21,7 +26,10 @@
|
|||
(let [shape (unchecked-get props "shape")
|
||||
{:keys [id x y width height]} shape
|
||||
transform (geom/transform-matrix shape)
|
||||
props (-> (attrs/extract-style-attrs shape)
|
||||
|
||||
gradient-id (when (:fill-color-gradient shape) (str (uuid/next)))
|
||||
|
||||
props (-> (attrs/extract-style-attrs shape gradient-id)
|
||||
(obj/merge!
|
||||
#js {:x x
|
||||
:y y
|
||||
|
@ -30,7 +38,12 @@
|
|||
:width width
|
||||
:height height}))]
|
||||
|
||||
[:& shape-custom-stroke {:shape shape
|
||||
:base-props props
|
||||
:elem-name "rect"}]))
|
||||
|
||||
[:*
|
||||
(when gradient-id
|
||||
[:& gradient {:id gradient-id
|
||||
:shape shape
|
||||
:gradient (:fill-color-gradient shape)}])
|
||||
[:& shape-custom-stroke {:shape shape
|
||||
:base-props props
|
||||
:elem-name "rect"}]]))
|
||||
|
|
|
@ -10,18 +10,20 @@
|
|||
(ns app.main.ui.workspace.colorpicker
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[app.main.store :as st]
|
||||
[okulary.core :as l]
|
||||
[cuerdas.core :as str]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.color :as uc]
|
||||
[app.main.ui.icons :as i]
|
||||
[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 dwc]
|
||||
[app.main.data.modal :as modal]
|
||||
[okulary.core :as l]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [t]]))
|
||||
|
||||
;; --- Refs
|
||||
|
@ -44,7 +46,7 @@
|
|||
|
||||
;; --- Color Picker Modal
|
||||
|
||||
(mf/defc value-selector [{:keys [hue saturation value on-change]}]
|
||||
(mf/defc value-saturation-selector [{:keys [hue saturation value on-change]}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
calculate-pos
|
||||
(fn [ev]
|
||||
|
@ -53,7 +55,7 @@
|
|||
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-selector
|
||||
[:div.value-saturation-selector
|
||||
{:on-mouse-down #(reset! dragging? true)
|
||||
:on-mouse-up #(reset! dragging? false)
|
||||
:on-pointer-down (partial dom/capture-pointer)
|
||||
|
@ -64,41 +66,389 @@
|
|||
:left (str (* 100 saturation) "%")
|
||||
:top (str (* 100 (- 1 (/ value 255))) "%")}}]]))
|
||||
|
||||
(mf/defc hue-selector [{:keys [hue on-change]}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
calculate-pos
|
||||
(fn [ev]
|
||||
(let [{:keys [left right]} (-> ev dom/get-target dom/get-bounding-rect)
|
||||
{:keys [x]} (-> ev dom/get-client-position)
|
||||
px (math/clamp (/ (- x left) (- right left)) 0 1)]
|
||||
(on-change (* px 360))))]
|
||||
[:div.hue-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 (* (/ hue 360) 100) "%")}}]]))
|
||||
|
||||
(mf/defc opacity-selector [{:keys [opacity on-change]}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
(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]
|
||||
(let [{:keys [left right]} (-> ev dom/get-target dom/get-bounding-rect)
|
||||
{:keys [x]} (-> ev dom/get-client-position)
|
||||
px (math/clamp (/ (- x left) (- right left)) 0 1)]
|
||||
(on-change px)))]
|
||||
[:div.opacity-selector
|
||||
{:on-mouse-down #(reset! dragging? true)
|
||||
(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 %))}
|
||||
[:div.handler {:style {:pointer-events "none"
|
||||
:left (str (* opacity 100) "%")}}]]))
|
||||
|
||||
(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")
|
||||
|
@ -108,12 +458,13 @@
|
|||
{:hex (or value "000000")
|
||||
:alpha (or opacity 1)
|
||||
:r r :g g :b b
|
||||
:h h :s s :v v}
|
||||
))
|
||||
:h h :s s :v v}))
|
||||
|
||||
(mf/defc colorpicker
|
||||
[{:keys [value opacity on-change on-accept]}]
|
||||
(let [current-color (mf/use-state (as-color-components value opacity))
|
||||
|
||||
active-tab (mf/use-state :ramp #_:harmony #_:hsva)
|
||||
selected-library (mf/use-state "recent")
|
||||
current-library-colors (mf/use-state [])
|
||||
ref-picker (mf/use-ref)
|
||||
|
@ -136,7 +487,16 @@
|
|||
parse-selected (fn [selected]
|
||||
(if (#{"recent" "file"} selected)
|
||||
(keyword selected)
|
||||
(uuid selected)) )]
|
||||
(uuid selected)) )
|
||||
|
||||
change-tab (fn [tab] #(reset! active-tab tab))
|
||||
|
||||
handle-change-color (fn [changes]
|
||||
(swap! current-color merge changes)
|
||||
(when (:hex changes)
|
||||
(reset! value-ref (:hex changes)))
|
||||
(on-change (:hex changes (:hex @current-color))
|
||||
(:alpha changes (:alpha @current-color))))]
|
||||
|
||||
;; Update state when there is a change in the props upstream
|
||||
(mf/use-effect
|
||||
|
@ -149,9 +509,19 @@
|
|||
(mf/deps @current-color)
|
||||
(fn [] (let [node (mf/ref-val ref-picker)
|
||||
rgb [(:r @current-color) (:g @current-color) (:b @current-color)]
|
||||
hue-rgb (uc/hsv->rgb [(:h @current-color) 1.0 255])]
|
||||
hue-rgb (uc/hsv->rgb [(:h @current-color) 1.0 255])
|
||||
hsl-from (uc/hsv->hsl [(:h @current-color) 0 (:v @current-color)])
|
||||
hsl-to (uc/hsv->hsl [(:h @current-color) 1 (:v @current-color)])
|
||||
|
||||
format-hsl (fn [[h s l]]
|
||||
(str/fmt "hsl(%s, %s, %s)"
|
||||
h
|
||||
(str (* s 100) "%")
|
||||
(str (* l 100) "%")))]
|
||||
(dom/set-css-property node "--color" (str/join ", " rgb))
|
||||
(dom/set-css-property node "--hue" (str/join ", " hue-rgb)))))
|
||||
(dom/set-css-property node "--hue-rgb" (str/join ", " hue-rgb))
|
||||
(dom/set-css-property node "--saturation-grad-from" (format-hsl hsl-from))
|
||||
(dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to)))))
|
||||
|
||||
;; Load library colors when the select is changed
|
||||
(mf/use-effect
|
||||
|
@ -204,168 +574,78 @@
|
|||
(on-change (:hex @current-color) (:alpha @current-color) nil nil picked-shift?))))
|
||||
|
||||
[:div.colorpicker {:ref ref-picker}
|
||||
[:div.top-actions
|
||||
[:button.picker-btn
|
||||
{:class (when picking-color? "active")
|
||||
:on-click (fn []
|
||||
(modal/allow-click-outside!)
|
||||
(st/emit! (dwc/start-picker)))}
|
||||
i/picker]]
|
||||
[:div.colorpicker-content
|
||||
[:div.top-actions
|
||||
[:button.picker-btn
|
||||
{:class (when picking-color? "active")
|
||||
:on-click (fn []
|
||||
(modal/allow-click-outside!)
|
||||
(st/emit! (dwc/start-picker)))}
|
||||
i/picker]
|
||||
|
||||
(if picking-color?
|
||||
[:div.picker-detail-wrapper
|
||||
[:div.center-circle]
|
||||
[:canvas#picker-detail {:width 200
|
||||
:height 160}]]
|
||||
[:& value-selector {:hue (:h @current-color)
|
||||
:saturation (:s @current-color)
|
||||
:value (:v @current-color)
|
||||
:on-change (fn [s v]
|
||||
(let [hex (uc/hsv->hex [(:h @current-color) s v])
|
||||
[r g b] (uc/hex->rgb hex)]
|
||||
(swap! current-color assoc
|
||||
:hex hex
|
||||
:r r :g g :b b
|
||||
:s s :v v)
|
||||
(reset! value-ref hex)
|
||||
(on-change hex (:alpha @current-color))))}])
|
||||
(when (not picking-color?)
|
||||
[:div.shade-selector
|
||||
[:div.color-bullet]
|
||||
[:& hue-selector {:hue (:h @current-color)
|
||||
:on-change (fn [h]
|
||||
(let [hex (uc/hsv->hex [h (:s @current-color) (:v @current-color)])
|
||||
[r g b] (uc/hex->rgb hex)]
|
||||
(swap! current-color assoc
|
||||
:hex hex
|
||||
:r r :g g :b b
|
||||
:h h )
|
||||
(reset! value-ref hex)
|
||||
(on-change hex (:alpha @current-color))))}]
|
||||
[:& opacity-selector {:opacity (:alpha @current-color)
|
||||
:on-change (fn [alpha]
|
||||
(swap! current-color assoc :alpha alpha)
|
||||
(on-change (:hex @current-color) alpha))}]])
|
||||
[:div.gradients-buttons
|
||||
[:button.gradient.linear-gradient #_{:class "active"}]
|
||||
[:button.gradient.radial-gradient]]]
|
||||
|
||||
[:div.color-values
|
||||
[:input.hex-value {:id "hex-value"
|
||||
:value (:hex @current-color)
|
||||
:on-change (fn [e]
|
||||
(let [val (-> e dom/get-target dom/get-value)
|
||||
val (if (= (first val) \#) val (str \# val))]
|
||||
(swap! current-color assoc :hex val)
|
||||
(when (uc/hex? val)
|
||||
(reset! value-ref val)
|
||||
(let [[r g b] (uc/hex->rgb val)
|
||||
[h s v] (uc/hex->hsv val)]
|
||||
#_[:div.gradient-stops
|
||||
[:div.gradient-background {:style {:background "linear-gradient(90deg, #EC0BE5, #CDCDCD)" }}]
|
||||
[:div.gradient-stop-wrapper
|
||||
[:div.gradient-stop.start {:style {:background-color "#EC0BE5"}}]
|
||||
[:div.gradient-stop.end {:style {:background-color "#CDCDCD"
|
||||
:left "100%"}}]]]
|
||||
|
||||
(if picking-color?
|
||||
[:div.picker-detail-wrapper
|
||||
[:div.center-circle]
|
||||
[:canvas#picker-detail {:width 200 :height 160}]]
|
||||
(case @active-tab
|
||||
:ramp [:& ramp-selector {:color @current-color :on-change handle-change-color}]
|
||||
:harmony [:& harmony-selector {:color @current-color :on-change handle-change-color}]
|
||||
:hsva [:& hsva-selector {:color @current-color :on-change handle-change-color}]
|
||||
nil))
|
||||
|
||||
[:& color-inputs {:type (if (= @active-tab :hsva) :hsv :rgb) :color @current-color :on-change handle-change-color}]
|
||||
|
||||
[:div.libraries
|
||||
[:select {:on-change (fn [e]
|
||||
(let [val (-> e dom/get-target dom/get-value)]
|
||||
(reset! selected-library val)))
|
||||
:value @selected-library}
|
||||
[:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")]
|
||||
[:option {:value "file"} (t locale "workspace.libraries.colors.file-library")]
|
||||
(for [[_ {:keys [name id]}] shared-libs]
|
||||
[:option {:key id
|
||||
:value id} name])]
|
||||
|
||||
[:div.selected-colors
|
||||
(when (= "file" @selected-library)
|
||||
[:div.color-bullet.button.plus-button {:style {:background-color "white"}
|
||||
:on-click #(st/emit! (dwl/add-color (:hex @current-color)))}
|
||||
i/plus])
|
||||
|
||||
[:div.color-bullet.button {:style {:background-color "white"}
|
||||
:on-click #(st/emit! (dwc/show-palette (parse-selected @selected-library)))}
|
||||
i/palette]
|
||||
|
||||
(for [[idx {:keys [id file-id value]}] (map-indexed vector @current-library-colors)]
|
||||
[:div.color-bullet {:key (str "color-" idx)
|
||||
:on-click (fn []
|
||||
(swap! current-color assoc :hex value)
|
||||
(reset! value-ref value)
|
||||
(let [[r g b] (uc/hex->rgb value)
|
||||
[h s v] (uc/hex->hsv value)]
|
||||
(swap! current-color assoc
|
||||
:r r :g g :b b
|
||||
:h h :s s :v v)
|
||||
(on-change val (:alpha @current-color))))))}]
|
||||
[:input.red-value {:id "red-value"
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 255
|
||||
:value (:r @current-color)
|
||||
:on-change (fn [e]
|
||||
(let [val (-> e dom/get-target dom/get-value (math/clamp 0 255))]
|
||||
(swap! current-color assoc :r val)
|
||||
(when (not (nil? val))
|
||||
(let [{:keys [g b]} @current-color
|
||||
hex (uc/rgb->hex [val g b])
|
||||
[h s v] (uc/hex->hsv hex)]
|
||||
(reset! value-ref hex)
|
||||
(swap! current-color assoc
|
||||
:hex hex
|
||||
:h h :s s :v v)
|
||||
(on-change hex (:alpha @current-color))))))}]
|
||||
[:input.green-value {:id "green-value"
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 255
|
||||
:value (:g @current-color)
|
||||
:on-change (fn [e]
|
||||
(let [val (-> e dom/get-target dom/get-value (math/clamp 0 255))]
|
||||
(swap! current-color assoc :g val)
|
||||
(when (not (nil? val))
|
||||
(let [{:keys [r b]} @current-color
|
||||
hex (uc/rgb->hex [r val b])
|
||||
[h s v] (uc/hex->hsv hex)]
|
||||
(reset! value-ref hex)
|
||||
(swap! current-color assoc
|
||||
:hex hex
|
||||
:h h :s s :v v)
|
||||
(on-change hex (:alpha @current-color))))))}]
|
||||
[:input.blue-value {:id "blue-value"
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 255
|
||||
:value (:b @current-color)
|
||||
:on-change (fn [e]
|
||||
(let [val (-> e dom/get-target dom/get-value (math/clamp 0 255))]
|
||||
(swap! current-color assoc :b val)
|
||||
(when (not (nil? val))
|
||||
(let [{:keys [r g]} @current-color
|
||||
hex (uc/rgb->hex [r g val])
|
||||
[h s v] (uc/hex->hsv hex)]
|
||||
(reset! value-ref hex)
|
||||
(swap! current-color assoc
|
||||
:hex hex
|
||||
:h h :s s :v v)
|
||||
(on-change hex (:alpha @current-color))))))}]
|
||||
[:input.alpha-value {:id "alpha-value"
|
||||
:type "number"
|
||||
:min 0
|
||||
:step 0.1
|
||||
:max 1
|
||||
:value (if (= (:alpha @current-color) :multiple)
|
||||
""
|
||||
(math/precision (:alpha @current-color) 2))
|
||||
:on-change (fn [e]
|
||||
(let [val (-> e dom/get-target dom/get-value (math/clamp 0 1))]
|
||||
(swap! current-color assoc :alpha val)
|
||||
(on-change (:hex @current-color) val)))}]
|
||||
[:label.hex-label {:for "hex-value"} "HEX"]
|
||||
[:label.red-label {:for "red-value"} "R"]
|
||||
[:label.green-label {:for "green-value"} "G"]
|
||||
[:label.blue-label {:for "blue-value"} "B"]
|
||||
[:label.alpha-label {:for "alpha-value"} "A"]]
|
||||
|
||||
[:div.libraries
|
||||
[:select {:on-change (fn [e]
|
||||
(let [val (-> e dom/get-target dom/get-value)]
|
||||
(reset! selected-library val)))
|
||||
:value @selected-library}
|
||||
[:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")]
|
||||
[:option {:value "file"} (t locale "workspace.libraries.colors.file-library")]
|
||||
(for [[_ {:keys [name id]}] shared-libs]
|
||||
[:option {:key id
|
||||
:value id} name])]
|
||||
|
||||
[:div.selected-colors
|
||||
(when (= "file" @selected-library)
|
||||
[:div.color-bullet.button.plus-button {:style {:background-color "white"}
|
||||
:on-click #(st/emit! (dwl/add-color (:hex @current-color)))}
|
||||
i/plus])
|
||||
|
||||
[:div.color-bullet.button {:style {:background-color "white"}
|
||||
:on-click #(st/emit! (dwc/show-palette (parse-selected @selected-library)))}
|
||||
i/palette]
|
||||
|
||||
(for [[idx {:keys [id file-id value]}] (map-indexed vector @current-library-colors)]
|
||||
[:div.color-bullet {:key (str "color-" idx)
|
||||
:on-click (fn []
|
||||
(swap! current-color assoc :hex value)
|
||||
(reset! value-ref value)
|
||||
(let [[r g b] (uc/hex->rgb value)
|
||||
[h s v] (uc/hex->hsv value)]
|
||||
(swap! current-color assoc
|
||||
:r r :g g :b b
|
||||
:h h :s s :v v)
|
||||
(on-change value (:alpha @current-color) id file-id)))
|
||||
:style {:background-color value}}])]
|
||||
|
||||
]
|
||||
(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
|
||||
|
|
292
frontend/src/app/main/ui/workspace/gradients.cljs
Normal file
292
frontend/src/app/main/ui/workspace/gradients.cljs
Normal file
|
@ -0,0 +1,292 @@
|
|||
;; 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.gradients
|
||||
"Gradients handlers and renders"
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
[beicon.core :as rx]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.common.math :as mth]
|
||||
[app.util.dom :as dom]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.matrix :as gmt]))
|
||||
|
||||
(def gradient-line-stroke-width 2)
|
||||
(def gradient-line-stroke-color "white")
|
||||
(def gradient-square-width 15)
|
||||
(def gradient-square-radius 2)
|
||||
(def gradient-square-stroke-width 2)
|
||||
(def gradient-width-handler-radius 5)
|
||||
(def gradient-width-handler-color "white")
|
||||
(def gradient-square-stroke-color "white")
|
||||
(def gradient-square-stroke-color-selected "#1FDEA7")
|
||||
|
||||
(mf/defc shadow [{:keys [id x y width height offset]}]
|
||||
[:filter {:id id
|
||||
:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:filterUnits "userSpaceOnUse"
|
||||
:color-interpolation-filters "sRGB"}
|
||||
[:feFlood {:flood-opacity "0" :result "BackgroundImageFix"}]
|
||||
[: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"}]
|
||||
[:feOffset {:dy offset}]
|
||||
[:feGaussianBlur {:stdDeviation "1"}]
|
||||
[:feColorMatrix {:type "matrix" :values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"}]
|
||||
[:feBlend {:mode "normal" :in2 "BackgroundImageFix" :result id}]
|
||||
[:feBlend {:mode "normal" :in "SourceGraphic" :in2 id :result "shape"}]])
|
||||
|
||||
(mf/defc gradient-line-drop-shadow-filter [{:keys [id zoom from-p to-p]}]
|
||||
[:& shadow
|
||||
{:id id
|
||||
:x (min (- (:x from-p) (/ 2 zoom))
|
||||
(- (:x to-p) (/ 2 zoom)))
|
||||
:y (min (- (:y from-p) (/ 2 zoom))
|
||||
(- (:y to-p) (/ 2 zoom)))
|
||||
:width (+ (mth/abs (- (:x to-p) (:x from-p))) (/ 4 zoom))
|
||||
:height (+ (mth/abs (- (:y to-p) (:y from-p))) (/ 4 zoom))
|
||||
:offset (/ 2 zoom)}])
|
||||
|
||||
|
||||
(mf/defc gradient-square-drop-shadow-filter [{:keys [id zoom point]}]
|
||||
[:& shadow
|
||||
{:id id
|
||||
:x (- (:x point) (/ gradient-square-width zoom 2) 2)
|
||||
:y (- (:y point) (/ gradient-square-width zoom 2) 2)
|
||||
:width (+ (/ gradient-square-width zoom) (/ 2 zoom) 4)
|
||||
:height (+ (/ gradient-square-width zoom) (/ 2 zoom) 4)
|
||||
:offset (/ 2 zoom)}])
|
||||
|
||||
(mf/defc gradient-width-handler-shadow-filter [{:keys [id zoom point]}]
|
||||
[:& shadow
|
||||
{:id id
|
||||
:x (- (:x point) (/ gradient-width-handler-radius zoom) 2)
|
||||
:y (- (:y point) (/ gradient-width-handler-radius zoom) 2)
|
||||
:width (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4)
|
||||
:height (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4)
|
||||
:offset (/ 2 zoom)}])
|
||||
|
||||
(def default-gradient
|
||||
{:type :linear
|
||||
:start-x 0.5 :start-y 0.5
|
||||
:end-x 0.5 :end-y 1
|
||||
:width 1.0
|
||||
:stops [{:offset 0
|
||||
:color "#FF0000"
|
||||
:opacity 1}
|
||||
{:offset 1
|
||||
:color "#FF0000"
|
||||
:opacity 0.2}]})
|
||||
|
||||
(def checkboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAACvUlEQVQoFQGyAk39AeLi4gAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB////AAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjScaa0cU7nIAAAAASUVORK5CYII=")
|
||||
|
||||
#_(def checkboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
|
||||
|
||||
(mf/defc gradient-color-handler
|
||||
[{:keys [filter-id zoom point color angle on-click on-mouse-down on-mouse-up]}]
|
||||
[:g {:filter (str/fmt "url(#%s)" filter-id)
|
||||
:transform (gmt/rotate-matrix angle point)}
|
||||
|
||||
[:image {:href checkboard
|
||||
:x (- (:x point) (/ gradient-square-width 2 zoom))
|
||||
:y (- (:y point) (/ gradient-square-width 2 zoom))
|
||||
:width (/ gradient-square-width zoom)
|
||||
:height (/ gradient-square-width zoom)}]
|
||||
|
||||
[:rect {:x (- (:x point) (/ gradient-square-width 2 zoom))
|
||||
:y (- (:y point) (/ gradient-square-width 2 zoom))
|
||||
:rx (/ gradient-square-radius zoom)
|
||||
:width (/ gradient-square-width zoom 2)
|
||||
:height (/ gradient-square-width zoom)
|
||||
:fill (:value color)
|
||||
:on-click (partial on-click :to-p)
|
||||
:on-mouse-down (partial on-mouse-down :to-p)
|
||||
:on-mouse-up (partial on-mouse-up :to-p)}]
|
||||
|
||||
[:rect {:x (- (:x point) (/ gradient-square-width 2 zoom))
|
||||
:y (- (:y point) (/ gradient-square-width 2 zoom))
|
||||
:rx (/ gradient-square-radius zoom)
|
||||
:width (/ gradient-square-width zoom)
|
||||
:height (/ gradient-square-width zoom)
|
||||
:stroke "white"
|
||||
:stroke-width (/ gradient-square-stroke-width zoom)
|
||||
:fill (:value color)
|
||||
:fill-opacity (:opacity color)
|
||||
:on-click on-click
|
||||
:on-mouse-down on-mouse-down
|
||||
:on-mouse-up on-mouse-up}]])
|
||||
|
||||
(mf/defc gradient-handler-transformed
|
||||
[{:keys [from-p to-p width-p from-color to-color zoom on-change-start on-change-finish on-change-width on-change-stop-color]}]
|
||||
(let [moving-point (mf/use-var nil)
|
||||
angle (+ 90 (gpt/angle from-p to-p))
|
||||
|
||||
on-click (fn [position event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event))
|
||||
|
||||
on-mouse-down (fn [position event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(reset! moving-point position))
|
||||
|
||||
on-mouse-up (fn [position event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(reset! moving-point nil))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @moving-point from-p to-p width-p)
|
||||
(fn []
|
||||
(let [subs (->> st/stream
|
||||
(rx/filter ms/pointer-event?)
|
||||
(rx/filter #(= :viewport (:source %)))
|
||||
(rx/map :pt)
|
||||
(rx/subs #(case @moving-point
|
||||
:from-p (when on-change-start (on-change-start %))
|
||||
:to-p (when on-change-finish (on-change-finish %))
|
||||
:width-p (when on-change-width
|
||||
(let [width-v (gpt/unit (gpt/to-vec from-p width-p))
|
||||
distance (gpt/point-line-distance % from-p to-p)
|
||||
new-width-p (gpt/add
|
||||
from-p
|
||||
(gpt/multiply width-v (gpt/point distance)))]
|
||||
(on-change-width new-width-p)))
|
||||
nil)))]
|
||||
(fn [] (rx/dispose! subs)))))
|
||||
[:g.gradient-handlers
|
||||
[:defs
|
||||
[:& gradient-line-drop-shadow-filter {:id "gradient_line_drop_shadow" :from-p from-p :to-p to-p :zoom zoom}]
|
||||
[:& gradient-line-drop-shadow-filter {:id "gradient_widh_line_drop_shadow" :from-p from-p :to-p width-p :zoom zoom}]
|
||||
[:& gradient-square-drop-shadow-filter {:id "gradient_square_from_drop_shadow" :point from-p :zoom zoom}]
|
||||
[:& gradient-square-drop-shadow-filter {:id "gradient_square_to_drop_shadow" :point to-p :zoom zoom}]
|
||||
[:& gradient-width-handler-shadow-filter {:id "gradient_width_handler_drop_shadow" :point width-p :zoom zoom}]]
|
||||
|
||||
[:g {:filter "url(#gradient_line_drop_shadow)"}
|
||||
[:line {:x1 (:x from-p)
|
||||
:y1 (:y from-p)
|
||||
:x2 (:x to-p)
|
||||
:y2 (:y to-p)
|
||||
:stroke gradient-line-stroke-color
|
||||
:stroke-width (/ gradient-line-stroke-width zoom)}]]
|
||||
|
||||
(when width-p
|
||||
[:g {:filter "url(#gradient_widh_line_drop_shadow)"}
|
||||
[:line {:x1 (:x from-p)
|
||||
:y1 (:y from-p)
|
||||
:x2 (:x width-p)
|
||||
:y2 (:y width-p)
|
||||
:stroke gradient-line-stroke-color
|
||||
:stroke-width (/ gradient-line-stroke-width zoom)}]])
|
||||
|
||||
(when width-p
|
||||
[:g {:filter "url(#gradient_width_handler_drop_shadow)"}
|
||||
[:circle {:cx (:x width-p)
|
||||
:cy (:y width-p)
|
||||
:r (/ gradient-width-handler-radius zoom)
|
||||
:fill gradient-width-handler-color
|
||||
:on-mouse-down (partial on-mouse-down :width-p)
|
||||
:on-mouse-up (partial on-mouse-up :width-p)}]])
|
||||
|
||||
[:& gradient-color-handler
|
||||
{:filter-id "gradient_square_from_drop_shadow"
|
||||
:zoom zoom
|
||||
:point from-p
|
||||
:color from-color
|
||||
:angle angle
|
||||
:on-click (partial on-click :from-p)
|
||||
:on-mouse-down (partial on-mouse-down :from-p)
|
||||
:on-mouse-up (partial on-mouse-up :from-p)}]
|
||||
|
||||
[:& gradient-color-handler
|
||||
{:filter-id "gradient_square_to_drop_shadow"
|
||||
:zoom zoom
|
||||
:point to-p
|
||||
:color to-color
|
||||
:angle angle
|
||||
:on-click (partial on-click :to-p)
|
||||
:on-mouse-down (partial on-mouse-down :to-p)
|
||||
:on-mouse-up (partial on-mouse-up :to-p)}]]))
|
||||
|
||||
(mf/defc gradient-handlers
|
||||
[{:keys [shape zoom]}]
|
||||
(let [{:keys [x y width height] :as sr} (:selrect shape)
|
||||
|
||||
state (mf/use-state (:fill-color-gradient shape default-gradient))
|
||||
|
||||
[{start-color :color start-opacity :opacity}
|
||||
{end-color :color end-opacity :opacity}] (:stops @state)
|
||||
|
||||
from-p (gpt/point (+ x (* width (:start-x @state)))
|
||||
(+ y (* height (:start-y @state))))
|
||||
|
||||
to-p (gpt/point (+ x (* width (:end-x @state)))
|
||||
(+ y (* height (:end-y @state))))
|
||||
|
||||
gradient-vec (gpt/to-vec from-p to-p)
|
||||
gradient-length (gpt/length gradient-vec)
|
||||
|
||||
width-v (-> gradient-vec
|
||||
(gpt/normal-left)
|
||||
(gpt/multiply (gpt/point (* (:width @state) (/ gradient-length (/ height 2) ))))
|
||||
(gpt/multiply (gpt/point (/ width 2))))
|
||||
|
||||
width-p (gpt/add from-p width-v)
|
||||
|
||||
on-change-start (fn [point]
|
||||
(let [start-x (/ (- (:x point) x) width)
|
||||
start-y (/ (- (:y point) y) height)]
|
||||
(swap! state assoc
|
||||
:start-x start-x
|
||||
:start-y start-y )))
|
||||
|
||||
on-change-finish (fn [point]
|
||||
(let [end-x (/ (- (:x point) x) width)
|
||||
end-y (/ (- (:y point) y) height)]
|
||||
(swap! state assoc
|
||||
:end-x end-x
|
||||
:end-y end-y)))
|
||||
|
||||
on-change-width (fn [point]
|
||||
(let [scale-factor-y (/ gradient-length (/ height 2))
|
||||
norm-dist (/ (gpt/distance point from-p)
|
||||
(* (/ width 2) scale-factor-y))]
|
||||
(swap! state assoc :width norm-dist)))
|
||||
|
||||
on-change-stop-color (fn [offset color opacity] (println "change-color"))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps shape)
|
||||
(fn []
|
||||
(reset! state (:fill-color-gradient shape default-gradient))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @state)
|
||||
(fn []
|
||||
(when (not= (:fill-color-gradient shape) @state)
|
||||
(st/emit! (dwc/update-shapes
|
||||
[(:id shape)]
|
||||
#(assoc % :fill-color-gradient @state))))))
|
||||
|
||||
[:& gradient-handler-transformed
|
||||
{:from-p from-p
|
||||
:to-p to-p
|
||||
:width-p (when (= :radial (:type @state)) width-p)
|
||||
:from-color {:value start-color :opacity start-opacity}
|
||||
:to-color {:value end-color :opacity end-opacity}
|
||||
:zoom zoom
|
||||
:on-change-start on-change-start
|
||||
:on-change-finish on-change-finish
|
||||
:on-change-width on-change-width
|
||||
:on-change-stop-color on-change-stop-color}]))
|
|
@ -16,18 +16,20 @@
|
|||
[rumext.alpha :as mf]
|
||||
[rumext.util :refer [map->obj]]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.cursors :as cur]
|
||||
[app.common.math :as mth]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.util.debug :refer [debug?]]
|
||||
[app.main.ui.workspace.shapes.outline :refer [outline]]))
|
||||
|
||||
[app.main.ui.workspace.shapes.outline :refer [outline]]
|
||||
[app.main.ui.workspace.gradients :refer [gradient-handlers]]))
|
||||
|
||||
(def rotation-handler-size 25)
|
||||
(def resize-point-radius 4)
|
||||
|
@ -168,7 +170,6 @@
|
|||
:cursor (if (#{:left :right} position)
|
||||
(cur/resize-ew rotation)
|
||||
(cur/resize-ns rotation)) }}]))
|
||||
|
||||
(mf/defc controls
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
@ -180,7 +181,9 @@
|
|||
current-transform (mf/deref refs/current-transform)
|
||||
|
||||
selrect (geom/shape->rect-shape shape)
|
||||
transform (geom/transform-matrix shape)]
|
||||
transform (geom/transform-matrix shape)
|
||||
|
||||
tr-shape (geom/transform-shape shape)]
|
||||
|
||||
(when (not (#{:move :rotate} current-transform))
|
||||
[:g.controls
|
||||
|
@ -190,8 +193,7 @@
|
|||
:transform transform
|
||||
:zoom zoom
|
||||
:color color}]
|
||||
[:& outline {:shape (geom/transform-shape shape)
|
||||
:color color}]
|
||||
[:& outline {:shape tr-shape :color color}]
|
||||
|
||||
;; Handlers
|
||||
(for [{:keys [type position props]} (handlers-for-selection selrect)]
|
||||
|
@ -208,7 +210,11 @@
|
|||
(case type
|
||||
:rotation (when (not= :frame (:type shape)) [:> rotation-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)
|
||||
(mf/defc path-edition-selection-handlers
|
||||
|
|
Loading…
Add table
Reference in a new issue