Implement color picker and brush color selection

This commit is contained in:
Igor Zinken
2020-12-18 12:18:19 +01:00
parent f3a67f3fda
commit aac98b7e2f
8 changed files with 208 additions and 20 deletions

View File

@@ -6,6 +6,18 @@ No, I'm building a tool that does the bare minimum what I require and what I don
find in other open source tools. That doesn't mean of course that contributions
related to Photoshop-esque features aren't welcomed.
### All self-written ?
Yep, though it helps having worked five years in the photo software industry and having
tackled problems before.
PhotoMound does however make use of the following excellent libraries to speed up its development:
* [Vue](https://github.com/vuejs/vue) with [Vuex](https://github.com/vuejs/vuex) and [VueI18n](https://github.com/kazupon/vue-i18n)
* [Pickr](https://github.com/Simonwep/pickr) by Simonwep
* [Vue slider component](https://github.com/NightCatSama/vue-slider-component) by NightCatSama
* [Vue search select](https://github.com/moreta/vue-search-select#readme) by Moreta
## Dropbox integration
Requires you to [register a client id or access token](https://www.dropbox.com/developers/apps).
@@ -40,7 +52,7 @@ npm run lint
* Unit tests for factories
* DrawableLayer should only draw when brush too lis active
* Canvas clearRect() is not doing full width and height ?
* Layer view in options-panel: allow naming, repositioning, toggle visibility, set as mask
* Layer view in options-panel: allow naming, repositioning, toggle visibility, change type (for masking), opacity
* Canvas util : store transparency of images
* Restored base64 images should be treated as binary once more (see layer-factory)
* Add brush options > size, transparency
@@ -53,6 +65,7 @@ npm run lint
* Export output to image file (by rendering document content on separate canvas instead of taking snapshot at incorrect scale)
* Load/save documents from/to Dropbox
* Use hand cursor when draggable
* Text editing using Google fonts!
* Use paint brush cursor when painting
* Add tools for layer rotation and scaling
* Implement clone brush

29
package-lock.json generated
View File

@@ -7,6 +7,7 @@
"": {
"version": "0.1.0",
"dependencies": {
"@simonwep/pickr": "^1.8.0",
"canvas": "^2.6.1",
"core-js": "^3.6.5",
"dropbox": "^8.2.0",
@@ -1420,6 +1421,15 @@
"node": ">= 6"
}
},
"node_modules/@simonwep/pickr": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.8.0.tgz",
"integrity": "sha512-VaSD7TwktOsro5nQ/FjRx5JAJ09k5CNfGRHacgVRxeVPolUQwelz1SjL8HAOKZwTSmcnIObptpHABQS4zgN7sw==",
"dependencies": {
"core-js": "^3.8.0",
"nanopop": "^2.1.0"
}
},
"node_modules/@soda/friendly-errors-webpack-plugin": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -11736,6 +11746,11 @@
"node": ">=0.10.0"
}
},
"node_modules/nanopop": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.1.0.tgz",
"integrity": "sha512-jGTwpFRexSH+fxappnGQtN9dspgE2ipa1aOjtR24igG0pv6JCxImIAmrLRHX+zUF5+1wtsFVbKyfP51kIGAVNw=="
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -19730,6 +19745,15 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true
},
"@simonwep/pickr": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.8.0.tgz",
"integrity": "sha512-VaSD7TwktOsro5nQ/FjRx5JAJ09k5CNfGRHacgVRxeVPolUQwelz1SjL8HAOKZwTSmcnIObptpHABQS4zgN7sw==",
"requires": {
"core-js": "^3.8.0",
"nanopop": "^2.1.0"
}
},
"@soda/friendly-errors-webpack-plugin": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -28488,6 +28512,11 @@
"to-regex": "^3.0.1"
}
},
"nanopop": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.1.0.tgz",
"integrity": "sha512-jGTwpFRexSH+fxappnGQtN9dspgE2ipa1aOjtR24igG0pv6JCxImIAmrLRHX+zUF5+1wtsFVbKyfP51kIGAVNw=="
},
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",

View File

@@ -9,7 +9,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"canvas": "^2.6.1",
"@simonwep/pickr": "^1.8.0",
"core-js": "^3.6.5",
"dropbox": "^8.2.0",
"register-service-worker": "^1.7.1",

View File

@@ -1,5 +1,6 @@
{
"en-US": {
"brush": "Brush"
"brush": "Brush",
"brushColor": "Brush color"
}
}

View File

@@ -23,13 +23,56 @@
<template>
<div class="tool-option">
<h3 v-t="'brush'"></h3>
<div class="wrapper input">
<label v-t="'brushColor'"></label>
<component
:is="colorPicker"
v-model="brushColor"
class="color-picker"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
import ToolTypes from "@/definitions/tool-types";
import DrawableLayer from "@/components/ui/zcanvas/drawable-layer";
import { runSpriteFn } from "@/factories/sprite-factory";
import messages from "./messages.json";
export default {
i18n: { messages },
computed: {
...mapGetters([
"brushOptions",
]),
colorPicker() {
return () => import( "@/components/ui/color-picker/color-picker" );
},
brushColor: {
get() {
return this.brushOptions.color;
},
set( value ) {
this.setToolOptionValue({
tool: ToolTypes.BRUSH,
option: "color",
value,
});
runSpriteFn( sprite => {
if ( sprite instanceof DrawableLayer ) {
sprite.cacheGradient( this.brushColor );
}
});
},
},
},
methods: {
...mapMutations([
"setToolOptionValue",
]),
},
};
</script>

View File

@@ -0,0 +1,93 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
<template>
<div class="color-picker">
<div ref="picker"></div>
</div>
</template>
<script>
import "@simonwep/pickr/dist/themes/nano.min.css";
import Pickr from "@simonwep/pickr/dist/pickr.es5.min"; // 3 x size of modern bundle
//import Pickr from "@simonwep/pickr";
let pickrInstance;
const FRAC_VALUE = 0;
export default {
props: {
// the value is represented as RGBA
value: {
type: String,
default: "rgba(255,255,255,1)",
},
},
mounted() {
pickrInstance = Pickr.create({
el: this.$refs.picker,
theme: "nano",
default: this.value,
defaultRepresentation: "HEX",
components: {
preview: true,
opacity: true,
hue: true,
interaction: {
rgba: true,
hex: true,
cmyk: true,
input: true,
save: true
}
}
});
pickrInstance.on( "save", this.saveColor.bind( this ));
},
destroy() {
pickrInstance?.destroyAndRemove();
},
methods: {
saveColor( value ) {
const rgba = value.toRGBA();
const red = rgba[ 0 ].toFixed( FRAC_VALUE );
const green = rgba[ 1 ].toFixed( FRAC_VALUE );
const blue = rgba[ 2 ].toFixed( FRAC_VALUE );
const alpha = rgba[ 3 ];
this.$emit( "input", `rgba(${red},${green},${blue},${alpha})` );
pickrInstance.hide();
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/_mixins";
.color-picker {
display: inline-block;
margin-top: -$spacing-small;
}
</style>

View File

@@ -41,35 +41,43 @@ function DrawableLayer( layer ) {
this.setDraggable( true );
// TODO: setters and cache for these
const radius = 30; // TODO: setter
const innerRadius = 5;
const outerRadius = 70;
const opacity = .5; // 0 - 1 range
ctx.globalAlpha = opacity;
const { cvs: brush, ctx: brushCtx } = createCanvas( radius * 2, radius * 2 );
this.halfRadius = 0;
// Radii of the white glow.
// Radius of the entire circle.
const { cvs: brush, ctx: brushCtx } = createCanvas( 10, 10 );
x = radius;
y = radius;
const gradient = brushCtx.createRadialGradient( x, y, innerRadius, x, y, outerRadius );
gradient.addColorStop( 0, 'rgba(255,0,0,1)' );
gradient.addColorStop( 1, 'rgba(255,255,255,1)' );
this.cacheGradient = function( color, radius = 30, innerRadius = 5, outerRadius = 70 )
{
x = radius;
y = radius;
brushCtx.arc( x, y, radius, 0, 2 * Math.PI );
// update brush Canvas size
brush.width = radius * 2;
brush.height = radius * 2;
brushCtx.fillStyle = gradient;
brushCtx.fill();
const gradient = brushCtx.createRadialGradient( x, y, innerRadius, x, y, outerRadius );
gradient.addColorStop( 0, color );
gradient.addColorStop( 1, 'rgba(255,255,255,1)' );
this.handleMove = function( x, y ) {
brushCtx.arc( x, y, radius, 0, 2 * Math.PI );
brushCtx.fillStyle = gradient;
brushCtx.fill();
this.halfRadius = radius / 2;
},
this.handleMove = function( x, y )
{
// TODO: this zoomFactor should be taken into account by handleInteraction of zCanvas !!
// BECAUSE IT DOES NOT WORK FOR HANDLEPRESS CURRENTLY
x /= this.canvas.zoomFactor;
y /= this.canvas.zoomFactor;
ctx.drawImage( brush, x - radius / 2, y - radius / 2 );
ctx.drawImage( brush, x - this.halfRadius, y - this.halfRadius );
}
this.cacheGradient( "rgba(255,0,0,1)" );
}
sprite.extend( DrawableLayer );
export default DrawableLayer;

View File

@@ -28,12 +28,13 @@ export default {
activeTool: null,
options: {
[ ToolTypes.ZOOM ] : { level: 1 },
[ ToolTypes.BRUSH ]: { },
[ ToolTypes.BRUSH ]: { color: "rgba(255,0,0,1)" },
}
},
getters: {
activeTool: state => state.activeTool,
zoomOptions: state => state.options[ ToolTypes.ZOOM ],
brushOptions: state => state.options[ ToolTypes.BRUSH ],
},
mutations: {
setActiveTool( state, tool ) {