mirror of
https://github.com/exogen/t2-model-skinner.git
synced 2026-01-19 19:24:44 +00:00
Add more tools for Metallic canvases
This commit is contained in:
parent
11b8a03ad9
commit
43a1aa89af
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
docs/_next/static/chunks/pages/index-ebc0557df1157d4e.js
Normal file
2
docs/_next/static/chunks/pages/index-ebc0557df1157d4e.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
|||
!function(){"use strict";var e,r,_,t,n,i,u,c={},o={};function __webpack_require__(e){var r=o[e];if(void 0!==r)return r.exports;var _=o[e]={id:e,loaded:!1,exports:{}},t=!0;try{c[e].call(_.exports,_,_.exports,__webpack_require__),t=!1}finally{t&&delete o[e]}return _.loaded=!0,_.exports}__webpack_require__.m=c,e=[],__webpack_require__.O=function(r,_,t,n){if(_){n=n||0;for(var i=e.length;i>0&&e[i-1][2]>n;i--)e[i]=e[i-1];e[i]=[_,t,n];return}for(var u=1/0,i=0;i<e.length;i++){for(var _=e[i][0],t=e[i][1],n=e[i][2],c=!0,o=0;o<_.length;o++)u>=n&&Object.keys(__webpack_require__.O).every(function(e){return __webpack_require__.O[e](_[o])})?_.splice(o--,1):(c=!1,n<u&&(u=n));if(c){e.splice(i--,1);var a=t()}}return a},__webpack_require__.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(r,{a:r}),r},__webpack_require__.d=function(e,r){for(var _ in r)__webpack_require__.o(r,_)&&!__webpack_require__.o(e,_)&&Object.defineProperty(e,_,{enumerable:!0,get:r[_]})},__webpack_require__.f={},__webpack_require__.e=function(e){return Promise.all(Object.keys(__webpack_require__.f).reduce(function(r,_){return __webpack_require__.f[_](e,r),r},[]))},__webpack_require__.u=function(e){return"static/chunks/"+(737===e?"fb7d5399":e)+"."+({250:"10c119307e239c98",737:"bc4a70b34221e8c8",767:"0dd6b240996f3455",848:"fc0fe21cdc2e6431"})[e]+".js"},__webpack_require__.miniCssF=function(e){return"static/css/"+({214:"922e89893536f2f9",888:"fa467350f3c0ad7f"})[e]+".css"},__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r={},_="_N_E:",__webpack_require__.l=function(e,t,n,i){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var u,c,o=document.getElementsByTagName("script"),a=0;a<o.length;a++){var p=o[a];if(p.getAttribute("src")==e||p.getAttribute("data-webpack")==_+n){u=p;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",u.timeout=120,__webpack_require__.nc&&u.setAttribute("nonce",__webpack_require__.nc),u.setAttribute("data-webpack",_+n),u.src=__webpack_require__.tu(e)),r[e]=[t];var onScriptComplete=function(_,t){u.onerror=u.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],u.parentNode&&u.parentNode.removeChild(u),n&&n.forEach(function(e){return e(t)}),_)return _(t)},f=setTimeout(onScriptComplete.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=onScriptComplete.bind(null,u.onerror),u.onload=onScriptComplete.bind(null,u.onload),c&&document.head.appendChild(u)},__webpack_require__.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},__webpack_require__.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},__webpack_require__.tt=function(){return void 0===t&&(t={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(t=trustedTypes.createPolicy("nextjs#bundler",t))),t},__webpack_require__.tu=function(e){return __webpack_require__.tt().createScriptURL(e)},__webpack_require__.p="/t2-model-skinner/_next/",n={272:0},__webpack_require__.f.j=function(e,r){var _=__webpack_require__.o(n,e)?n[e]:void 0;if(0!==_){if(_)r.push(_[2]);else if(272!=e){var t=new Promise(function(r,t){_=n[e]=[r,t]});r.push(_[2]=t);var i=__webpack_require__.p+__webpack_require__.u(e),u=Error();__webpack_require__.l(i,function(r){if(__webpack_require__.o(n,e)&&(0!==(_=n[e])&&(n[e]=void 0),_)){var t=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;u.message="Loading chunk "+e+" failed.\n("+t+": "+i+")",u.name="ChunkLoadError",u.type=t,u.request=i,_[1](u)}},"chunk-"+e,e)}else n[e]=0}},__webpack_require__.O.j=function(e){return 0===n[e]},i=function(e,r){var _,t,i=r[0],u=r[1],c=r[2],o=0;if(i.some(function(e){return 0!==n[e]})){for(_ in u)__webpack_require__.o(u,_)&&(__webpack_require__.m[_]=u[_]);if(c)var a=c(__webpack_require__)}for(e&&e(r);o<i.length;o++)t=i[o],__webpack_require__.o(n,t)&&n[t]&&n[t][0](),n[t]=0;return __webpack_require__.O(a)},(u=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(i.bind(null,0)),u.push=i.bind(null,u.push.bind(u))}();
|
||||
//# sourceMappingURL=webpack-dd07d6e501d595e1.js.map
|
||||
!function(){"use strict";var e,r,_,t,n,i,u,c={},o={};function __webpack_require__(e){var r=o[e];if(void 0!==r)return r.exports;var _=o[e]={id:e,loaded:!1,exports:{}},t=!0;try{c[e].call(_.exports,_,_.exports,__webpack_require__),t=!1}finally{t&&delete o[e]}return _.loaded=!0,_.exports}__webpack_require__.m=c,e=[],__webpack_require__.O=function(r,_,t,n){if(_){n=n||0;for(var i=e.length;i>0&&e[i-1][2]>n;i--)e[i]=e[i-1];e[i]=[_,t,n];return}for(var u=1/0,i=0;i<e.length;i++){for(var _=e[i][0],t=e[i][1],n=e[i][2],c=!0,o=0;o<_.length;o++)u>=n&&Object.keys(__webpack_require__.O).every(function(e){return __webpack_require__.O[e](_[o])})?_.splice(o--,1):(c=!1,n<u&&(u=n));if(c){e.splice(i--,1);var a=t()}}return a},__webpack_require__.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(r,{a:r}),r},__webpack_require__.d=function(e,r){for(var _ in r)__webpack_require__.o(r,_)&&!__webpack_require__.o(e,_)&&Object.defineProperty(e,_,{enumerable:!0,get:r[_]})},__webpack_require__.f={},__webpack_require__.e=function(e){return Promise.all(Object.keys(__webpack_require__.f).reduce(function(r,_){return __webpack_require__.f[_](e,r),r},[]))},__webpack_require__.u=function(e){return"static/chunks/"+(737===e?"fb7d5399":e)+"."+({250:"10c119307e239c98",737:"bc4a70b34221e8c8",767:"0dd6b240996f3455",848:"fc0fe21cdc2e6431"})[e]+".js"},__webpack_require__.miniCssF=function(e){return"static/css/"+({214:"922e89893536f2f9",888:"02ded4c5652f0944"})[e]+".css"},__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r={},_="_N_E:",__webpack_require__.l=function(e,t,n,i){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var u,c,o=document.getElementsByTagName("script"),a=0;a<o.length;a++){var p=o[a];if(p.getAttribute("src")==e||p.getAttribute("data-webpack")==_+n){u=p;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",u.timeout=120,__webpack_require__.nc&&u.setAttribute("nonce",__webpack_require__.nc),u.setAttribute("data-webpack",_+n),u.src=__webpack_require__.tu(e)),r[e]=[t];var onScriptComplete=function(_,t){u.onerror=u.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],u.parentNode&&u.parentNode.removeChild(u),n&&n.forEach(function(e){return e(t)}),_)return _(t)},f=setTimeout(onScriptComplete.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=onScriptComplete.bind(null,u.onerror),u.onload=onScriptComplete.bind(null,u.onload),c&&document.head.appendChild(u)},__webpack_require__.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},__webpack_require__.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},__webpack_require__.tt=function(){return void 0===t&&(t={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(t=trustedTypes.createPolicy("nextjs#bundler",t))),t},__webpack_require__.tu=function(e){return __webpack_require__.tt().createScriptURL(e)},__webpack_require__.p="/t2-model-skinner/_next/",n={272:0},__webpack_require__.f.j=function(e,r){var _=__webpack_require__.o(n,e)?n[e]:void 0;if(0!==_){if(_)r.push(_[2]);else if(272!=e){var t=new Promise(function(r,t){_=n[e]=[r,t]});r.push(_[2]=t);var i=__webpack_require__.p+__webpack_require__.u(e),u=Error();__webpack_require__.l(i,function(r){if(__webpack_require__.o(n,e)&&(0!==(_=n[e])&&(n[e]=void 0),_)){var t=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;u.message="Loading chunk "+e+" failed.\n("+t+": "+i+")",u.name="ChunkLoadError",u.type=t,u.request=i,_[1](u)}},"chunk-"+e,e)}else n[e]=0}},__webpack_require__.O.j=function(e){return 0===n[e]},i=function(e,r){var _,t,i=r[0],u=r[1],c=r[2],o=0;if(i.some(function(e){return 0!==n[e]})){for(_ in u)__webpack_require__.o(u,_)&&(__webpack_require__.m[_]=u[_]);if(c)var a=c(__webpack_require__)}for(e&&e(r);o<i.length;o++)t=i[o],__webpack_require__.o(n,t)&&n[t]&&n[t][0](),n[t]=0;return __webpack_require__.O(a)},(u=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(i.bind(null,0)),u.push=i.bind(null,u.push.bind(u))}();
|
||||
//# sourceMappingURL=webpack-3b398f57a6bbe648.js.map
|
||||
File diff suppressed because one or more lines are too long
2
docs/_next/static/css/02ded4c5652f0944.css
Normal file
2
docs/_next/static/css/02ded4c5652f0944.css
Normal file
File diff suppressed because one or more lines are too long
1
docs/_next/static/css/02ded4c5652f0944.css.map
Normal file
1
docs/_next/static/css/02ded4c5652f0944.css.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
self.__BUILD_MANIFEST=function(s){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,"static/chunks/e21e5bbe-b28e0079b469d4e8.js","static/chunks/ebc70433-4eccd1cb3af29a3e.js","static/chunks/6eb5140f-31a2b2da7903b885.js","static/chunks/85d7bc83-1ca530d7d3f44153.js","static/chunks/3a17f596-9aeae038dfa51955.js","static/chunks/f580fadb-2911e2fbf64aae5a.js","static/chunks/515-13ff0773d41722ae.js","static/chunks/pages/index-1a2b7dc61d221db6.js"],"/_error":["static/chunks/pages/_error-54b9fcf45cb5bc62.js"],"/gallery":[s,"static/chunks/737a5600-aea383aaa2061cc6.js","static/chunks/918-3c6747f76df39072.js","static/css/922e89893536f2f9.css","static/chunks/pages/gallery-af1406fdc1af13f5.js"],sortedPages:["/","/_app","/_error","/gallery"]}}("static/chunks/cb355538-e538db8a1761f402.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|
||||
self.__BUILD_MANIFEST=function(s){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,"static/chunks/e21e5bbe-b28e0079b469d4e8.js","static/chunks/ebc70433-4eccd1cb3af29a3e.js","static/chunks/6eb5140f-31a2b2da7903b885.js","static/chunks/85d7bc83-1ca530d7d3f44153.js","static/chunks/3a17f596-9aeae038dfa51955.js","static/chunks/f580fadb-2911e2fbf64aae5a.js","static/chunks/515-13ff0773d41722ae.js","static/chunks/pages/index-ebc0557df1157d4e.js"],"/_error":["static/chunks/pages/_error-54b9fcf45cb5bc62.js"],"/gallery":[s,"static/chunks/737a5600-aea383aaa2061cc6.js","static/chunks/918-3c6747f76df39072.js","static/css/922e89893536f2f9.css","static/chunks/pages/gallery-af1406fdc1af13f5.js"],sortedPages:["/","/_app","/_error","/gallery"]}}("static/chunks/cb355538-e538db8a1761f402.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,7 +5,7 @@ import useTools from "./useTools";
|
|||
import { fabric } from "fabric";
|
||||
import { createFabricImage } from "./fabricUtils";
|
||||
|
||||
type JSONSnapshot = ReturnType<typeof Canvas.prototype["toJSON"]>;
|
||||
type JSONSnapshot = ReturnType<(typeof Canvas.prototype)["toJSON"]>;
|
||||
|
||||
function updateObjectControlOptions() {
|
||||
fabric.Object.prototype.set({
|
||||
|
|
@ -165,6 +165,10 @@ export default function Canvas({
|
|||
useEffect(() => {
|
||||
if (canvas) {
|
||||
canvas.isDrawingMode = isDrawingMode;
|
||||
if (isDrawingMode) {
|
||||
canvas.discardActiveObject();
|
||||
canvas.requestRenderAll();
|
||||
}
|
||||
}
|
||||
}, [canvas, isDrawingMode]);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export default function CanvasInteractions({
|
|||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const {
|
||||
activeCanvas,
|
||||
activeCanvasType,
|
||||
bringForward,
|
||||
sendBackward,
|
||||
duplicate,
|
||||
|
|
@ -143,14 +144,14 @@ export default function CanvasInteractions({
|
|||
break;
|
||||
}
|
||||
case "p": {
|
||||
if (activeCanvas === "metallic") {
|
||||
if (activeCanvasType === "metallic") {
|
||||
event.preventDefault();
|
||||
setDrawingMode(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "s":
|
||||
if (activeCanvas === "color") {
|
||||
if (activeCanvasType === "metallic") {
|
||||
event.preventDefault();
|
||||
setDrawingMode(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { InputHTMLAttributes, useEffect, useRef, useState } from "react";
|
||||
import { fabric } from "fabric";
|
||||
import getConfig from "next/config";
|
||||
import useCanvas from "./useCanvas";
|
||||
import useTools from "./useTools";
|
||||
|
|
@ -52,6 +53,8 @@ export default function CanvasTools() {
|
|||
setSaturation,
|
||||
brightness,
|
||||
setBrightness,
|
||||
contrast,
|
||||
setContrast,
|
||||
layerMode,
|
||||
setLayerMode,
|
||||
activeCanvasType,
|
||||
|
|
@ -108,6 +111,12 @@ export default function CanvasTools() {
|
|||
? selectedObjects.every((object) => lockedObjects.has(object))
|
||||
: false;
|
||||
|
||||
const hasSelection = selectedObjects.length > 0;
|
||||
|
||||
const selectionHasImages =
|
||||
selectedObjects.filter((object) => object instanceof fabric.Image).length >
|
||||
0;
|
||||
|
||||
const handleBackgroundColorChange: InputHTMLAttributes<HTMLInputElement>["onChange"] =
|
||||
(event) => {
|
||||
setBackgroundColor(event.target.value);
|
||||
|
|
@ -168,359 +177,36 @@ export default function CanvasTools() {
|
|||
</label>
|
||||
</div>
|
||||
<div className="Buttons">
|
||||
{activeCanvasType === "color" ? (
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
onChange={async (event) => {
|
||||
const imageUrl = await new Promise<string>(
|
||||
(resolve, reject) => {
|
||||
const inputFile = event.target.files?.[0];
|
||||
if (inputFile) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", (event) => {
|
||||
resolve(event.target?.result as string);
|
||||
});
|
||||
reader.readAsDataURL(inputFile);
|
||||
} else {
|
||||
reject(new Error("No input file provided."));
|
||||
}
|
||||
}
|
||||
);
|
||||
addImages([imageUrl]);
|
||||
}}
|
||||
type="file"
|
||||
accept=".png, image/png"
|
||||
hidden
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Add Image"
|
||||
title="Add Image"
|
||||
onClick={() => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ImPlus style={{ fontSize: 14 }} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
data-active={isFilterToolsOpen ? "" : undefined}
|
||||
aria-label="Filters"
|
||||
title="Filters"
|
||||
onClick={() => {
|
||||
setFilterToolsOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
>
|
||||
<ImContrast />
|
||||
</button>
|
||||
|
||||
{isFilterToolsOpen ? (
|
||||
<div
|
||||
className="BrushToolsPopup"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
tabIndex={-1}
|
||||
onBlur={(event) => {
|
||||
const newFocusElement = event.relatedTarget;
|
||||
const isFocusLeaving =
|
||||
!newFocusElement ||
|
||||
!event.currentTarget.contains(newFocusElement);
|
||||
if (isFocusLeaving) {
|
||||
setFilterToolsOpen(false);
|
||||
}
|
||||
}}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="Fields">
|
||||
<div className="Field ApplyTo">
|
||||
<label>Layer:</label>
|
||||
<ul>
|
||||
{selectedObjects.length ? (
|
||||
<li>
|
||||
<input
|
||||
type="radio"
|
||||
name="FilterLayer"
|
||||
value="SelectedLayer"
|
||||
id="FilterLayer-SelectedLayer"
|
||||
checked={layerMode === "SelectedLayer"}
|
||||
onChange={() => {
|
||||
setLayerMode("SelectedLayer");
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="FilterLayer-SelectedLayer">
|
||||
selected ({selectedObjects.length.toLocaleString()})
|
||||
</label>
|
||||
</li>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
<input
|
||||
type="radio"
|
||||
name="FilterLayer"
|
||||
value="BaseLayer"
|
||||
id="FilterLayer-BaseLayer"
|
||||
checked={layerMode === "BaseLayer"}
|
||||
onChange={() => {
|
||||
setLayerMode("BaseLayer");
|
||||
}}
|
||||
/>{" "}
|
||||
<label htmlFor="FilterLayer-BaseLayer">base</label>
|
||||
</li>
|
||||
<li>
|
||||
<input
|
||||
type="radio"
|
||||
name="FilterLayer"
|
||||
value="AllLayers"
|
||||
id="FilterLayer-AllLayers"
|
||||
checked={layerMode === "AllLayers"}
|
||||
onChange={() => {
|
||||
setLayerMode("AllLayers");
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="FilterLayer-AllLayers">
|
||||
all (
|
||||
{canvas?._objects.length.toLocaleString() ?? 0})
|
||||
</label>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="Field">
|
||||
<label>
|
||||
Hue:{" "}
|
||||
<strong>
|
||||
{hueRotate == null ? (
|
||||
"MULTIPLE VALUES"
|
||||
) : (
|
||||
<>{Math.round(hueRotate * 180)}°</>
|
||||
)}
|
||||
</strong>
|
||||
</label>
|
||||
<div className="SliderContainer">
|
||||
<Slider
|
||||
min={-180}
|
||||
max={180}
|
||||
startPoint={0}
|
||||
value={Math.round((hueRotate ?? 0) * 180)}
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
setHueRotate(value / 180);
|
||||
}}
|
||||
trackStyle={{
|
||||
height: 8,
|
||||
background: "#03fccf",
|
||||
}}
|
||||
handleStyle={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginTop: -6,
|
||||
borderColor: "#03fccf",
|
||||
background: "rgb(5, 69, 76)",
|
||||
// background: `rgb(${brushColor}, ${brushColor}, ${brushColor})`,
|
||||
opacity: 1,
|
||||
}}
|
||||
railStyle={{
|
||||
height: 8,
|
||||
border: "1px solid #555",
|
||||
background: "rgba(255, 255, 255, 0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Field">
|
||||
<label>
|
||||
Saturation:{" "}
|
||||
<strong>
|
||||
{saturation == null
|
||||
? "MULTIPLE VALUES"
|
||||
: `${Math.round(saturation * 100 + 100)}%`}
|
||||
</strong>
|
||||
</label>
|
||||
<div className="SliderContainer">
|
||||
<Slider
|
||||
min={-100}
|
||||
max={100}
|
||||
startPoint={0}
|
||||
value={Math.round((saturation ?? 0) * 100)}
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
setSaturation(value / 100);
|
||||
}}
|
||||
trackStyle={{
|
||||
height: 8,
|
||||
background: "#03fccf",
|
||||
}}
|
||||
handleStyle={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginTop: -6,
|
||||
borderColor: "#03fccf",
|
||||
background: "rgb(5, 69, 76)",
|
||||
// background: `rgb(${brushColor}, ${brushColor}, ${brushColor})`,
|
||||
opacity: 1,
|
||||
}}
|
||||
railStyle={{
|
||||
height: 8,
|
||||
border: "1px solid #555",
|
||||
background: "rgba(255, 255, 255, 0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Field">
|
||||
<label>
|
||||
Brightness:{" "}
|
||||
<strong>
|
||||
{brightness == null
|
||||
? "MULTIPLE VALUES"
|
||||
: `${Math.round(brightness * 100 + 100)}%`}
|
||||
</strong>
|
||||
</label>
|
||||
<div className="SliderContainer">
|
||||
<Slider
|
||||
min={-100}
|
||||
max={100}
|
||||
startPoint={0}
|
||||
value={Math.round((brightness ?? 0) * 100)}
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
setBrightness(value / 100);
|
||||
}}
|
||||
trackStyle={{
|
||||
height: 8,
|
||||
background: "#03fccf",
|
||||
}}
|
||||
handleStyle={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginTop: -6,
|
||||
borderColor: "#03fccf",
|
||||
background: "rgb(5, 69, 76)",
|
||||
// background: `rgb(${brushColor}, ${brushColor}, ${brushColor})`,
|
||||
opacity: 1,
|
||||
}}
|
||||
railStyle={{
|
||||
height: 8,
|
||||
border: "1px solid #555",
|
||||
background: "rgba(255, 255, 255, 0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isSelectionLocked ? "Unlock" : "Lock"}
|
||||
title={isSelectionLocked ? "Unlock (L)" : "Lock (L)"}
|
||||
onClick={isSelectionLocked ? unlockSelection : lockSelection}
|
||||
data-locked={isSelectionLocked ? "" : undefined}
|
||||
>
|
||||
{isSelectionLocked ? (
|
||||
<FaUnlock style={{ fontSize: 14 }} />
|
||||
) : (
|
||||
<FaLock style={{ fontSize: 14 }} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Bring Forward"
|
||||
title="Bring Forward (F)"
|
||||
onClick={bringForward}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Send Backward"
|
||||
title="Send Backward (B)"
|
||||
onClick={sendBackward}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Duplicate"
|
||||
title="Duplicate (D)"
|
||||
onClick={duplicate}
|
||||
>
|
||||
<RiFileCopyFill />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete"
|
||||
title="Delete (Backspace)"
|
||||
onClick={deleteSelection}
|
||||
disabled={isSelectionLocked}
|
||||
>
|
||||
<FaTrashAlt />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Undo"
|
||||
title={`Undo (${commandKeyPrefix}Z)`}
|
||||
onClick={undo}
|
||||
disabled={!canUndo}
|
||||
>
|
||||
<ImUndo2 />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Redo"
|
||||
title={`Redo (${
|
||||
isMac
|
||||
? `${shiftKeySymbol}${commandKeyPrefix}Z)`
|
||||
: `${commandKeyPrefix} Y`
|
||||
})`}
|
||||
onClick={redo}
|
||||
disabled={!canRedo}
|
||||
>
|
||||
<ImRedo2 />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{activeCanvasType === "metallic" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-active={isDrawingMode ? undefined : ""}
|
||||
aria-label="Select"
|
||||
title="Select (S)"
|
||||
onClick={() => {
|
||||
setDrawingMode(false);
|
||||
}}
|
||||
>
|
||||
<GiArrowCursor />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
data-active={isDrawingMode ? "" : undefined}
|
||||
aria-label="Paint"
|
||||
title="Paint (P)"
|
||||
onClick={() => {
|
||||
setDrawingMode(true);
|
||||
setBrushToolsOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
>
|
||||
<IoMdBrush />
|
||||
</button>
|
||||
<div className="ButtonGroup">
|
||||
<button
|
||||
className="ButtonGroup"
|
||||
type="button"
|
||||
data-active={isDrawingMode ? undefined : ""}
|
||||
aria-label="Select Mode"
|
||||
title="Select Mode (S)"
|
||||
onClick={() => {
|
||||
setDrawingMode(false);
|
||||
}}
|
||||
>
|
||||
<GiArrowCursor />
|
||||
</button>
|
||||
<button
|
||||
className="ButtonGroup"
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
data-active={isDrawingMode ? "" : undefined}
|
||||
aria-label="Paint Mode"
|
||||
title="Paint Mode (P)"
|
||||
onClick={() => {
|
||||
setDrawingMode(true);
|
||||
setBrushToolsOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
>
|
||||
<IoMdBrush />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isBrushToolsOpen ? (
|
||||
<div
|
||||
|
|
@ -613,6 +299,389 @@ export default function CanvasTools() {
|
|||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
onChange={async (event) => {
|
||||
const imageUrl = await new Promise<string>((resolve, reject) => {
|
||||
const inputFile = event.target.files?.[0];
|
||||
if (inputFile) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", (event) => {
|
||||
resolve(event.target?.result as string);
|
||||
});
|
||||
reader.readAsDataURL(inputFile);
|
||||
} else {
|
||||
reject(new Error("No input file provided."));
|
||||
}
|
||||
});
|
||||
addImages([imageUrl]);
|
||||
}}
|
||||
type="file"
|
||||
accept=".png, image/png"
|
||||
hidden
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Add Image"
|
||||
title="Add Image"
|
||||
onClick={() => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ImPlus style={{ fontSize: 14 }} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
data-active={isFilterToolsOpen ? "" : undefined}
|
||||
aria-label="Filters"
|
||||
title="Filters"
|
||||
disabled={!selectionHasImages}
|
||||
onClick={() => {
|
||||
setFilterToolsOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
>
|
||||
<ImContrast />
|
||||
</button>
|
||||
|
||||
{isFilterToolsOpen ? (
|
||||
<div
|
||||
className="BrushToolsPopup"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
tabIndex={-1}
|
||||
onBlur={(event) => {
|
||||
const newFocusElement = event.relatedTarget;
|
||||
const isFocusLeaving =
|
||||
!newFocusElement ||
|
||||
!event.currentTarget.contains(newFocusElement);
|
||||
if (isFocusLeaving) {
|
||||
setFilterToolsOpen(false);
|
||||
}
|
||||
}}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="Fields">
|
||||
<div className="Field ApplyTo">
|
||||
<label>Layer:</label>
|
||||
<ul>
|
||||
{selectedObjects.length ? (
|
||||
<li>
|
||||
<input
|
||||
type="radio"
|
||||
name="FilterLayer"
|
||||
value="SelectedLayer"
|
||||
id="FilterLayer-SelectedLayer"
|
||||
checked={layerMode === "SelectedLayer"}
|
||||
onChange={() => {
|
||||
setLayerMode("SelectedLayer");
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="FilterLayer-SelectedLayer">
|
||||
selected ({selectedObjects.length.toLocaleString()})
|
||||
</label>
|
||||
</li>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
<input
|
||||
type="radio"
|
||||
name="FilterLayer"
|
||||
value="BaseLayer"
|
||||
id="FilterLayer-BaseLayer"
|
||||
checked={layerMode === "BaseLayer"}
|
||||
onChange={() => {
|
||||
setLayerMode("BaseLayer");
|
||||
}}
|
||||
/>{" "}
|
||||
<label htmlFor="FilterLayer-BaseLayer">base</label>
|
||||
</li>
|
||||
<li>
|
||||
<input
|
||||
type="radio"
|
||||
name="FilterLayer"
|
||||
value="AllLayers"
|
||||
id="FilterLayer-AllLayers"
|
||||
checked={layerMode === "AllLayers"}
|
||||
onChange={() => {
|
||||
setLayerMode("AllLayers");
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="FilterLayer-AllLayers">
|
||||
all (
|
||||
{canvas?._objects
|
||||
.filter(
|
||||
(object) => object instanceof fabric.Image
|
||||
)
|
||||
.length.toLocaleString() ?? 0}
|
||||
)
|
||||
</label>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
{activeCanvasType === "color" ? (
|
||||
<>
|
||||
<div className="Field">
|
||||
<label>
|
||||
Hue:{" "}
|
||||
<strong>
|
||||
{hueRotate == null ? (
|
||||
"MULTIPLE VALUES"
|
||||
) : (
|
||||
<>{Math.round(hueRotate * 180)}°</>
|
||||
)}
|
||||
</strong>
|
||||
</label>
|
||||
<div className="SliderContainer">
|
||||
<Slider
|
||||
min={-180}
|
||||
max={180}
|
||||
startPoint={0}
|
||||
value={Math.round((hueRotate ?? 0) * 180)}
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
setHueRotate(value / 180);
|
||||
}}
|
||||
trackStyle={{
|
||||
height: 8,
|
||||
background: "#03fccf",
|
||||
}}
|
||||
handleStyle={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginTop: -6,
|
||||
borderColor: "#03fccf",
|
||||
background: "rgb(5, 69, 76)",
|
||||
// background: `rgb(${brushColor}, ${brushColor}, ${brushColor})`,
|
||||
opacity: 1,
|
||||
}}
|
||||
railStyle={{
|
||||
height: 8,
|
||||
border: "1px solid #555",
|
||||
background: "rgba(255, 255, 255, 0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Field">
|
||||
<label>
|
||||
Saturation:{" "}
|
||||
<strong>
|
||||
{saturation == null
|
||||
? "MULTIPLE VALUES"
|
||||
: `${Math.round(saturation * 100 + 100)}%`}
|
||||
</strong>
|
||||
</label>
|
||||
<div className="SliderContainer">
|
||||
<Slider
|
||||
min={-100}
|
||||
max={100}
|
||||
startPoint={0}
|
||||
value={Math.round((saturation ?? 0) * 100)}
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
setSaturation(value / 100);
|
||||
}}
|
||||
trackStyle={{
|
||||
height: 8,
|
||||
background: "#03fccf",
|
||||
}}
|
||||
handleStyle={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginTop: -6,
|
||||
borderColor: "#03fccf",
|
||||
background: "rgb(5, 69, 76)",
|
||||
// background: `rgb(${brushColor}, ${brushColor}, ${brushColor})`,
|
||||
opacity: 1,
|
||||
}}
|
||||
railStyle={{
|
||||
height: 8,
|
||||
border: "1px solid #555",
|
||||
background: "rgba(255, 255, 255, 0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="Field">
|
||||
<label>
|
||||
Brightness:{" "}
|
||||
<strong>
|
||||
{brightness == null
|
||||
? "MULTIPLE VALUES"
|
||||
: `${Math.round(brightness * 100 + 100)}%`}
|
||||
</strong>
|
||||
</label>
|
||||
<div className="SliderContainer">
|
||||
<Slider
|
||||
min={-100}
|
||||
max={100}
|
||||
startPoint={0}
|
||||
value={Math.round((brightness ?? 0) * 100)}
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
setBrightness(value / 100);
|
||||
}}
|
||||
trackStyle={{
|
||||
height: 8,
|
||||
background: "#03fccf",
|
||||
}}
|
||||
handleStyle={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginTop: -6,
|
||||
borderColor: "#03fccf",
|
||||
background: "rgb(5, 69, 76)",
|
||||
// background: `rgb(${brushColor}, ${brushColor}, ${brushColor})`,
|
||||
opacity: 1,
|
||||
}}
|
||||
railStyle={{
|
||||
height: 8,
|
||||
border: "1px solid #555",
|
||||
background: "rgba(255, 255, 255, 0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Field">
|
||||
<label>
|
||||
Contrast:{" "}
|
||||
<strong>
|
||||
{contrast == null
|
||||
? "MULTIPLE VALUES"
|
||||
: `${Math.round(contrast * 100 + 100)}%`}
|
||||
</strong>
|
||||
</label>
|
||||
<div className="SliderContainer">
|
||||
<Slider
|
||||
min={-100}
|
||||
max={100}
|
||||
startPoint={0}
|
||||
value={Math.round((contrast ?? 0) * 100)}
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
setContrast(value / 100);
|
||||
}}
|
||||
trackStyle={{
|
||||
height: 8,
|
||||
background: "#03fccf",
|
||||
}}
|
||||
handleStyle={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginTop: -6,
|
||||
borderColor: "#03fccf",
|
||||
background: "rgb(5, 69, 76)",
|
||||
// background: `rgb(${brushColor}, ${brushColor}, ${brushColor})`,
|
||||
opacity: 1,
|
||||
}}
|
||||
railStyle={{
|
||||
height: 8,
|
||||
border: "1px solid #555",
|
||||
background: "rgba(255, 255, 255, 0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isSelectionLocked ? "Unlock" : "Lock"}
|
||||
title={isSelectionLocked ? "Unlock (L)" : "Lock (L)"}
|
||||
onClick={isSelectionLocked ? unlockSelection : lockSelection}
|
||||
data-locked={isSelectionLocked ? "" : undefined}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
{isSelectionLocked ? (
|
||||
<FaUnlock style={{ fontSize: 14 }} />
|
||||
) : (
|
||||
<FaLock style={{ fontSize: 14 }} />
|
||||
)}
|
||||
</button>
|
||||
<div className="ButtonGroup">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Bring Forward"
|
||||
title="Bring Forward (F)"
|
||||
onClick={bringForward}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Send Backward"
|
||||
title="Send Backward (B)"
|
||||
onClick={sendBackward}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Duplicate"
|
||||
title="Duplicate (D)"
|
||||
onClick={duplicate}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
<RiFileCopyFill />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete"
|
||||
title="Delete (Backspace)"
|
||||
onClick={deleteSelection}
|
||||
disabled={isSelectionLocked || !hasSelection}
|
||||
>
|
||||
<FaTrashAlt />
|
||||
</button>
|
||||
<div className="ButtonGroup">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Undo"
|
||||
title={`Undo (${commandKeyPrefix}Z)`}
|
||||
onClick={undo}
|
||||
disabled={!canUndo}
|
||||
>
|
||||
<ImUndo2 />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Redo"
|
||||
title={`Redo (${
|
||||
isMac
|
||||
? `${shiftKeySymbol}${commandKeyPrefix}Z)`
|
||||
: `${commandKeyPrefix} Y`
|
||||
})`}
|
||||
onClick={redo}
|
||||
disabled={!canRedo}
|
||||
>
|
||||
<ImRedo2 />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
<div className="Export">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ type ObjectFilters = {
|
|||
HueRotation?: number;
|
||||
Saturation?: number;
|
||||
Brightness?: number;
|
||||
Contrast?: number;
|
||||
};
|
||||
|
||||
export default function ToolsProvider({ children }: { children: ReactNode }) {
|
||||
|
|
@ -107,8 +108,8 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
const { canvases } = useCanvas();
|
||||
const { canvas, notifyChange, undo, redo, canUndo, canRedo } =
|
||||
useCanvas(activeCanvas);
|
||||
const { canvas: metallicCanvas } = useCanvas(metallicCanvasId);
|
||||
const [isDrawingMode, setDrawingMode] = useState(false);
|
||||
const { canvas: metallicCanvas, setDrawingMode } =
|
||||
useCanvas(metallicCanvasId);
|
||||
const { combineColorAndAlphaImageUrls } = useImageWorker();
|
||||
const { canvasPadding } = useSettings();
|
||||
const [filterChanges, setFilterChanges] = useState<
|
||||
|
|
@ -126,7 +127,33 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
}
|
||||
|
||||
const getFilter = (name: keyof ObjectFilters) => {
|
||||
const setFilter = useCallback(
|
||||
(name: keyof ObjectFilters, value: number) => {
|
||||
const filterChanges: Array<[fabric.Object, ObjectFilters]> = [];
|
||||
const newFilterMap = new Map(filterMap);
|
||||
let applyObjects = selectedObjects;
|
||||
if (layerMode === "AllLayers") {
|
||||
applyObjects = canvas?._objects ?? [];
|
||||
} else if (layerMode === "BaseLayer") {
|
||||
applyObjects = canvas?._objects.slice(0, 1) ?? [];
|
||||
}
|
||||
for (const applyObject of applyObjects) {
|
||||
if (applyObject instanceof fabric.Image) {
|
||||
const existingFilters = filterMap.get(applyObject) ?? {};
|
||||
const newFilters = { ...existingFilters, [name]: value };
|
||||
newFilterMap.set(applyObject, newFilters);
|
||||
filterChanges.push([applyObject, newFilters]);
|
||||
}
|
||||
}
|
||||
setFilterMap(newFilterMap);
|
||||
setFilterChanges(filterChanges);
|
||||
},
|
||||
[canvas, layerMode, filterMap, selectedObjects]
|
||||
);
|
||||
|
||||
const getFilter = (
|
||||
name: keyof ObjectFilters
|
||||
): [number | null, (value: number) => void] => {
|
||||
let applyObjects = selectedObjects;
|
||||
if (layerMode === "AllLayers") {
|
||||
applyObjects = canvas?._objects ?? [];
|
||||
|
|
@ -142,54 +169,18 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
.slice(1)
|
||||
.every((applyObject, i) => getValue(i + 1) === firstValue)
|
||||
) {
|
||||
return firstValue;
|
||||
return [firstValue, (value: number) => setFilter(name, value)];
|
||||
}
|
||||
return null;
|
||||
return [null, (value: number) => setFilter(name, value)];
|
||||
} else {
|
||||
return 0;
|
||||
return [0, (value: number) => setFilter(name, value)];
|
||||
}
|
||||
};
|
||||
|
||||
const hueRotate = getFilter("HueRotation");
|
||||
const saturation = getFilter("Saturation");
|
||||
const brightness = getFilter("Brightness");
|
||||
|
||||
const setFilter = useCallback(
|
||||
(name: keyof ObjectFilters, value: number) => {
|
||||
const filterChanges: Array<[fabric.Object, ObjectFilters]> = [];
|
||||
const newFilterMap = new Map(filterMap);
|
||||
let applyObjects = selectedObjects;
|
||||
if (layerMode === "AllLayers") {
|
||||
applyObjects = canvas?._objects ?? [];
|
||||
} else if (layerMode === "BaseLayer") {
|
||||
applyObjects = canvas?._objects.slice(0, 1) ?? [];
|
||||
}
|
||||
for (const applyObject of applyObjects) {
|
||||
const existingFilters = filterMap.get(applyObject) ?? {};
|
||||
const newFilters = { ...existingFilters, [name]: value };
|
||||
newFilterMap.set(applyObject, newFilters);
|
||||
filterChanges.push([applyObject, newFilters]);
|
||||
}
|
||||
setFilterMap(newFilterMap);
|
||||
setFilterChanges(filterChanges);
|
||||
},
|
||||
[canvas, layerMode, filterMap, selectedObjects]
|
||||
);
|
||||
|
||||
const setHueRotate = useCallback(
|
||||
(value: number) => setFilter("HueRotation", value),
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const setSaturation = useCallback(
|
||||
(value: number) => setFilter("Saturation", value),
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const setBrightness = useCallback(
|
||||
(value: number) => setFilter("Brightness", value),
|
||||
[setFilter]
|
||||
);
|
||||
const [hueRotate, setHueRotate] = getFilter("HueRotation");
|
||||
const [saturation, setSaturation] = getFilter("Saturation");
|
||||
const [brightness, setBrightness] = getFilter("Brightness");
|
||||
const [contrast, setContrast] = getFilter("Contrast");
|
||||
|
||||
useEffect(() => {
|
||||
if (!filterChanges.length) {
|
||||
|
|
@ -198,9 +189,13 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
for (const [selectedObject, newFilters] of filterChanges) {
|
||||
if (selectedObject instanceof fabric.Image) {
|
||||
selectedObject.filters = [];
|
||||
if (activeCanvasType === "metallic") {
|
||||
const grayscaleFilter = new fabric.Image.filters.Grayscale();
|
||||
selectedObject.filters.push(grayscaleFilter);
|
||||
}
|
||||
for (const key in newFilters) {
|
||||
const filterValue = newFilters[key as keyof ObjectFilters] ?? 0;
|
||||
if (filterValue !== 0) {
|
||||
const filterValue = newFilters[key as keyof ObjectFilters];
|
||||
if (filterValue != null) {
|
||||
switch (key) {
|
||||
case "HueRotation":
|
||||
selectedObject.filters.push(
|
||||
|
|
@ -224,6 +219,13 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
})
|
||||
);
|
||||
break;
|
||||
case "Contrast":
|
||||
selectedObject.filters.push(
|
||||
new fabric.Image.filters.Contrast({
|
||||
contrast: filterValue,
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +236,7 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
if (notifyChange) {
|
||||
notifyChange();
|
||||
}
|
||||
}, [filterChanges, notifyChange]);
|
||||
}, [filterChanges, activeCanvasType, notifyChange]);
|
||||
|
||||
const lockSelection = useCallback(() => {
|
||||
if (selectedObjects.length) {
|
||||
|
|
@ -319,7 +321,7 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
canvas.setActiveObject(lastAddedImage);
|
||||
}
|
||||
},
|
||||
[canvas, activeCanvasType, textureSize]
|
||||
[canvas, activeCanvasType, setDrawingMode, textureSize]
|
||||
);
|
||||
|
||||
const duplicate = useCallback(async () => {
|
||||
|
|
@ -509,6 +511,8 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
setSaturation,
|
||||
brightness,
|
||||
setBrightness,
|
||||
contrast,
|
||||
setContrast,
|
||||
layerMode,
|
||||
setLayerMode,
|
||||
selectedObjects,
|
||||
|
|
@ -524,8 +528,6 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
canUndo,
|
||||
canRedo,
|
||||
exportSkin,
|
||||
isDrawingMode,
|
||||
setDrawingMode,
|
||||
selectedMaterialIndex,
|
||||
setSelectedMaterialIndex,
|
||||
textureSize,
|
||||
|
|
@ -547,10 +549,12 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
hueRotate,
|
||||
saturation,
|
||||
brightness,
|
||||
contrast,
|
||||
layerMode,
|
||||
setHueRotate,
|
||||
setSaturation,
|
||||
setBrightness,
|
||||
setContrast,
|
||||
selectedObjects,
|
||||
lockSelection,
|
||||
unlockSelection,
|
||||
|
|
@ -564,7 +568,6 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
|
|||
canUndo,
|
||||
canRedo,
|
||||
exportSkin,
|
||||
isDrawingMode,
|
||||
selectedMaterialIndex,
|
||||
textureSize,
|
||||
hasMetallic,
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ select {
|
|||
place-content: center;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid rgb(23, 159, 138);
|
||||
border-radius: 2px;
|
||||
border-radius: 3px;
|
||||
width: 28px;
|
||||
height: 24px;
|
||||
background: rgb(74, 193, 171);
|
||||
|
|
@ -279,6 +279,23 @@ select {
|
|||
color: white;
|
||||
}
|
||||
|
||||
.ButtonGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.ButtonGroup button:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.ButtonGroup button:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.CanvasToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ interface ToolsContextValue {
|
|||
setSaturation: (saturation: number) => void;
|
||||
brightness: number | null;
|
||||
setBrightness: (brightness: number) => void;
|
||||
contrast: number | null;
|
||||
setContrast: (contrast: number) => void;
|
||||
layerMode: string;
|
||||
setLayerMode: (layerMode: string) => void;
|
||||
deleteSelection: () => void;
|
||||
|
|
|
|||
Loading…
Reference in a new issue