デザイナーがVue.jsで大きなカラーピッカーを作ってみた
こんにちは。デザイナーのKです。
本記事では、JavaScriptフレームワーク「Vue.js」の実験+学習を兼ねて行った、オリジナルカラーピッカー作成の概要をご紹介します。
カラーピッカーとは上図のような、色を調整したりカラーコードを拾ったりするためのツールです。
大抵のデザインツールにはデフォルトで付属しているので、わざわざ自作する必要は無いのですが、かねてから自分専用の「痒いところに手が届く」カラーピッカーが欲しかったことと、機能・規模的にVue.jsの導入教材としてちょうどいいサイズに思い、開発に挑戦しました。
求める機能はこんなかんじです。
- でかい
- 手軽に起動
- CSSコーディングに最適化
DEMO
早速ですが成果物をご覧ください。Chrome以外での動作は保証しません。
See the Pen Big Color Picker by makudo (@makudo) on CodePen.
動作のあやしいところはあるものの、おおむね一般的なカラーピッカーと同等の挙動になっているはずです。
大きさはAdobeのカラーピッカーのおよそ2倍弱で、存在感のあるサイズではないでしょうか?
大きいと何が嬉しいのかと言うと、単純に気分がいいというだけです。大きいからより細かく色指定ができるというわけではありません。
工夫したポイントについて図解します。
上図の赤枠部分が自分にとっての「痒い所に手が届く」以前から欲しかった機能です。
ささやかな機能ですが、これによりCSSコーディングの効率が若干向上します。
大したツールではありませんが、「冷蔵庫の残り物だけでサッと美味しい夜食を作れた」的な、
自己満足のいく仕上がりとなりました。
Vue.jsとは
Vue.jsとはインタラクティブなUIを構築するためのJavaScriptフレームワークです。
DOMとデータの同期のためにリアクティブなデータバインディングを提供することが特長の1つです。
リアクティブとは
正確な定義を無視して、かなり大雑把に説明すると、この記事で扱う「リアクティブ」とは
「Aを変更したら連動してBが変わる」
「Bを変更したら連動してAが変わる」
という挙動・仕組みのことを指します。
よく使われる例えは、「エクセルでセルの値を変更すると連動して別のセルの値が変化する」という挙動です。
カラーピッカーツール開発においては
「カーソルの位置に応じてカラーコードの値が変化する」
「カラーコードの値に応じてカーソルの位置が変化する」
という挙動の設計ために「リアクティブなデータバインディング」が役立つはずです。
jQueryじゃだめなのか?
実はこのツールは、当初はjQueryで開発していましたが、途中で断念しています。
基本的な機能は実装できたのですが、私の能力では複数のカラーコードとカーソルのリアクティブな挙動の設計は難易度が高く、
「これを動かしたら連動してここが変化して…」という動作を追加していくうちにイベント管理が死ぬほど煩雑になり、挫折しました。
もちろん、jQueryでもしっかり設計すれば問題なく実現できるはずですが、
リアクティブなデータバインディングはVue.jsのほうが圧倒的に簡単でした。
※ 正確には「リアクティブな挙動」と「リアクティブなプログラミング」は別の概念だそうですが、ここでは簡便のために敢えて混同しています。
見た目を作る
挙動を実装する前に、HTMLとCSSで見た目を作ります。
隠し課題として、今回は画像を一切使わずにCSSだけで表現できないかチャレンジしてみました。
自己満足が理由の大半ですが、CSSのみで構築することで今後の取り回しやメンテナンス性にも恩恵がもたらされるはずです。
予想していた以上に苦戦しましたが、なんとかHTML+CSSだけで組むことができました。
無駄なこだわりを発揮してAdobeライクなスキンを採用。
See the Pen Big Color Picker by makudo (@makudo) on CodePen.
特に苦労したのは彩度+明度を調整する正方形の領域です。
ここは複雑なグラデーションでできており、CSSのみでの表現は困難に思われましたが、
よく観察すると下図のような構成になっていることに気づきました。
これならCSSだけでなんとかできそうです。
結果、以下のように指定して実現しました。
1 2 |
background-color: #ff0000; background-image: linear-gradient(rgba(0, 0, 0, 0), rgb(0, 0, 0)),linear-gradient(to right, rgb(255, 255, 255), rgba(255, 255, 255, 0)); |
グラデーション部分は固定で、background-color
の変更で色相変化に対応できます。
Vue.jsの準備
※この記事は私なりのVue.jsの使い方、気づいたポイントなどの紹介がメインで、Vue.jsそのものの解説は主目的としていません。用語や説明内容に誤りが含まれている可能性は高いと思われますので、予めご容赦ください。
最小構成を用意
空のHTMLファイルを用意します。
手軽に使いたいのでVue.jsはCDNで読み込みます。
1 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.js"></script> |
Vue.jsで構築する領域(コンテナー)を用意します。
1 |
<div id="app"></div> |
以下のようにVueインスタンスを作成し、el
オプションに上記コンテナーのidを指定してVueインスタンスを紐付けます。
1 2 3 4 5 |
<script> new Vue({ el: '#app' }) </script> |
これで開発の準備ができました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<html> <head> <style> /* 省略 */ </style> </head> <body> <div id="app"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.js"></script> <script> new Vue({ el: '#app' }) </script> </body> </html> |
Vue.js Devtools
事前にVue.jsデバッグ用のChrome拡張をインストールしておくと便利です。
基本的な使い方
data, methods, computed, templateオプション
このカラーピッカーツールでは上記4種類のオプションを利用しています。
私はそれぞれ以下のような理解のもと使い分けています。
オプション | 説明 |
data | 変数置き場。Vueインスタンス内で利用する変数(stateというそうです)を定義。 オブジェクトを返す関数として記述する。 |
methods | メソッド置き場。イベント発火の処理、引数がある処理、戻り値の無い処理を記述する(ことにしている)。 |
computed | 算出プロパティ置き場。計算結果を返す。変数のように使う関数。methodsよりも処理効率が良いらしい。 devtoolsで値の参照ができるのでここに書ける処理はなるべくここに書く |
template | HTMLを書く場所。この内容がelオプションで指定したコンテナー内に描画される。 |
methods
とcomputed
についての解釈が怪しいですが、一応このような区別で利用しています。
以下は説明のためにサンプルとして作ったリンゴの値段計算機です。
120円 × 追加数 × 消費税8%の値段を表示します。
See the Pen Apple Calc by makudo (@makudo) on CodePen.
template
部で、{{ }} 記法でdata
やcomputed
の値を参照できます。値の変更を検知して動的に書き換わります。v-on
ディレクティブを使うとイベントを登録できます。v-on:click="plus1"
で「クリックされたらplus1メソッドを実行」となります。methods
やcomputed
でstate
を参照する場合は頭にthis
を付けます。{{result}}
にはcomputed
での計算結果が返ります。
jQueryのように、DOM要素を直接操作するのではなく、data
の監視と検知が自動的に(?)行われ、処理が実行されることがポイントです。
カラーピッカー解説
前置きが長くなりましたが、ここからカラーピッカーの主要部分について、いくつかピックアップして解説します。
支配的rgbオブジェクトで全体を管理
jQueryでの開発時の失敗は、カーソルの位置やカラーコードを決定する際に、統一的に管理されているデータを参照するのではなく、場当たり的・その場しのぎ的な処理を重ねてしまい、全体が混乱したことでした。
それを踏まえ、今回のツールでは、全ての要素のふるまいを決定するrgbオブジェクトを用意しました。
1 2 3 4 5 6 7 8 9 |
data: function() { return { rgb: { r:255, g:0, b:0 } } } |
どのような変更処理も必ずこのオブジェクトと手続きが必要、という官僚的な仕組みを採用することでイベント処理が増大しても混乱が軽減できるはずです。
しかしうまくいかなかった
カーソルの座標の制御にはHSV(Hue(色相)Saturation(彩度)Value(明度))という色モデルを利用し、
適宜RGBに変換、またはその逆をする必要があります。
そのHSV色も常に支配的rgbを参照して管理したかったのですが、
RGB←→HSV変換がどうしてもうまく行かず(※)、結局rgbオブジェクトと同等の支配力を持つhsvオブジェクトを用意せざるをえませんでした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
data: function(){ return { rgb: { r:255, g:0, b:0 }, hsv: { h:360, s:100, v:100 }, } } |
そして、rgbに変更が発生したらhsvも変更する(またはその逆)という処理を手動で実装しています。
Vue.jsのうまみを減衰させる手続きですが、他に妙案が思いつきませんでした。
rgbオブジェクトから色を表示
現在色のプレビューと16進数コードの表示は以下のように書きます。
単純に、rgbの値を16進数に変換してHTMLに突っ込むということをやっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
data: function(){ return //省略 }, computed: { //rgbの値を16進数コードに変換 rgb2hex: function(){ return //省略 }, }, template: ` <div v-bind:style="{backgroundColor:'#' + rgb2hex}"></div> //プレビュー枠 <input type="text" v-bind:value="'#' + rgb2hex"> //16進数表示テキストボックス ` |
v-bind
ディレクティブを利用すると要素の属性で式を利用できます。
クリック座標からカーソルと16進数コードを変更
カラーピッカーをクリックしたら、座標を元に色を計算するという処理を作ります。
カーソル位置の制御にはHSV色を使う必要があると説明しましたが、それは、HSVがカラーピッカーの値と下図のように対応しているからです。
X軸とY軸に対応しているので、クリック座標からそれぞれの色要素への変換ができます。
以下のコードでこのような処理を行います。
- クリックでcolorSelectメソッド実行
- クリック座標からhsv(のsとv)を変更 → hsvの値をもとにrgbを変更
- hsvの変更を検知してカーソルの位置が変化 & rgbの変更を検知してプレビュー色&コードが変化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
methods:{ //マウスダウン座標からhsvとrgbを変更 colorSelect: function(){ this.hsv.s = Math.round(event.offsetX * this.colorUnit); this.hsv.v = Math.round(Math.abs((event.offsetY * this.colorUnit)-100)); this.rgb = this.hsv2rgb; }, }, computed: { //s、vの1単位あたりのピクセル数を返す colorUnit: function(){ return 100 / this.colorPickerSize }, //hsvの値からカーソル座標を計算して返す colorCursor: function(){ return { left: (this.hsv.s / this.colorUnit), top: Math.abs((this.hsv.v / this.colorUnit) - this.colorPickerSize) } }, //hsvの値をrgbオブジェクトに変換 hsv2rgb: function(){ return //省略 } //rgbの値を16進数コードに変換 rgb2hex: function(){ return //省略 }, }, template: ` <div class="colorpicker" v-on:mousedown="colorSelect"> <div class="colorpicker-cursor" v-bind:style="{left:colorCursor.left + 'px',top:colorCursor.top + 'px'}"></div> </div> <div class="preview" v-bind:style="{backgroundColor:'#' + rgb2hex}"></div> <input type="text" class="colorcode-hex" v-bind:value="'#' + rgb2hex"> ` |
※微調整や例外処理などの記述は削除しています。
methods
ではdata
の変更を行うだけcomputed
でdata
の値からカーソルの位置を計算するtemplate
ではイベントの登録とcomputed
の参照を設定する
というように処理を切り分けられるため、非常にスッキリと記述することができます。
特にmethods部分でDOMのことを考える必要が無いということが、大きなストレス軽減となりました。
その他の処理
上記の考え方の応用で以下の処理も実装できます。
- 色相操作
- ドラッグ操作
- 16進数コード入力
- その他カラーコード及び不透明度操作
- 上下キーで値の変更
- 例外処理
完成
See the Pen Big Color Picker by makudo (@makudo) on CodePen.
そしてさらにこれをこねくり回してブックマークレットを作りました。よろしければご笑納ください。
1 |
javascript:(function(f){var style = document.createElement("style");var cssStr = ".bigcolorpickercontainer{position:fixed;z-index:100000;top:20px;right:20px;padding:10px 20px 55px 55px;text-align:center;border-radius:12px 12px 5px 5px;background:rgb(53, 53, 53);box-shadow:rgba(0,0,0,0.5) 0px 5px 30px,rgba(255,255,255,1) 0 1px 1px inset,rgba(0,0,0,0.3) 0 -2px 1px inset;}";cssStr += ".bigcolorpickercontainer .title{user-select: none;display:inline-block;text-align:center;color:rgb(204,204,204);font-family:arial;padding-bottom:11px;width:100%;margin:10px 0;font-size:18px;}";cssStr += ".bigcolorpickercontainer .remove-btn{user-select: none;position:absolute;top:5px;left:7px;margin:0;line-height:30px;color:#bbb;font-size:30px;cursor:pointer;width:30px;height:30px}";cssStr += ".bigcolorpickercontainer .remove-btn:hover{color:#fff}";cssStr += ".bigcolorpickercontainer .inner{display:flex;justify-content:space-between;align-items:flex-start;}";cssStr += ".bigcolorpickercontainer .colorpicker-wrap{background-color:#fff;background-image:linear-gradient(45deg,#ccc 25%,transparent 25%,transparent 75%,#ccc 75%,#ccc),linear-gradient(45deg,#ccc 25%,transparent 25%,transparent 75%,#ccc 75%,#ccc);background-position:0 0,12px 12px;background-size:24px 24px;}";cssStr += ".bigcolorpickercontainer .colorpicker{position:relative;overflow:hidden;background-color:rgb(255,0,0);background-image:linear-gradient(rgba(0,0,0,0),rgb(0,0,0)),linear-gradient(to right,rgb(255,255,255),rgba(255,255,255,0));}";cssStr += ".bigcolorpickercontainer .colorpicker-cursor{pointer-events: none;position:absolute;width:17px;height:17px;left:490px;top:-9px;box-sizing:border-box;background:linear-gradient(transparent 46%, white 46%, white 53%, transparent 53%), linear-gradient(90deg, transparent 47%, white 47%, white 53%, transparent 53%);border:1px solid white;border-radius:50%;}";cssStr += ".bigcolorpickercontainer .colorpicker-cursor.black{border-color:black;background:linear-gradient(transparent 46%,black 46%,black 53%,transparent 53%),linear-gradient(90deg,transparent 47%,black 47%,black 53%,transparent 53%);}";cssStr += ".bigcolorpickercontainer .huepicker{position:relative;width:100px;background-image:linear-gradient(hsl(360,100%,50%),hsl(330,100%,50%),hsl(300,100%,50%),hsl(270,100%,50%),hsl(240,100%,50%),hsl(210,100%,50%),hsl(180,100%,50%),hsl(150,100%,50%),hsl(120,100%,50%),hsl(90,100%,50%),hsl(60,100%,50%),hsl(30,100%,50%),hsl(0,100%,50%));}";cssStr += ".bigcolorpickercontainer .huepicker-cursor{position:absolute;right:-13px;border-right:10px solid rgb(255,255,255);border-top:7px solid transparent;border-bottom:7px solid transparent;}";cssStr += ".bigcolorpickercontainer .preview-wrap{margin-right:20px;margin-bottom:20px;background-color:#fff;background-image:linear-gradient(45deg,#ccc 25%,transparent 25%,transparent 75%,#ccc 75%,#ccc),linear-gradient(45deg,#ccc 25%,transparent 25%,transparent 75%,#ccc 75%,#ccc);background-position:0 0,10px 10px;background-size:20px 20px;}";cssStr += ".bigcolorpickercontainer .preview{width:160px;height:120px;background:rgb(255,0,0);}";cssStr += ".bigcolorpickercontainer .colorcode-wrap{color:rgb(255,255,255);font-size:17px;}";cssStr += ".bigcolorpickercontainer .colorcode-boxes-wrap{display:flex;flex-direction:row;}";cssStr += ".bigcolorpickercontainer .colorcode-boxes{flex-basis:48%;margin-right:18px;margin-bottom:15px;}";cssStr += ".bigcolorpickercontainer input{border-radius:2px;border:1px solid #777;background:#555;color:#FFF;}";cssStr += ".bigcolorpickercontainer .colorcode-box span{display:inline-block;width:18px;font-size:16px;}";cssStr += ".bigcolorpickercontainer .colorcode-box input{display:inline-block;padding:5px 7px;width:34px;margin-bottom:10px;margin-left:3px;font-size:17px;}";cssStr += ".bigcolorpickercontainer .colorcode-rgb{display:block;width:148px;margin-bottom:15px;padding:8px 5px;font-size:13px;}";cssStr += ".bigcolorpickercontainer .colorcode-hex{display:block;width:148px;padding:5px;font-size:17px;}";style.innerHTML = cssStr;var wrap = document.createElement("div");wrap.id = "bigcolorpicker-wrap";var vuejs = document.createElement("script");vuejs.src = "https://unpkg.com/vue/dist/vue.js";var el = document.createElement("div");el.id="app";vuejs.onload=function(){f();};document.body.appendChild(wrap);wrap.appendChild(style);wrap.appendChild(vuejs);wrap.appendChild(el);})(function(){new Vue({el: '#app',data: function() {return {rgb: {r:255,g:0,b:0},hsv: {h:360,s:100,v:100},alpha: 1,colorPickerSize:460,hueChangeFlg:false,hueCursorClickFlg:false,hueCursorSize:14,colorChangeFlg:false,colorCursorSize:16,}},methods: {hueSelectStart: function(){this.hueChangeFlg = true;var ajustY = 0;if(this.hueCursorClickFlg){ajustY = this.hueCursorPos + this.hueCursorSize/2;}this.hsv.h = Math.round(Math.abs(((event.offsetY + ajustY) * this.hueUnit)-360));this.rgb = this.hsv2rgb(this.hsv.h,this.hsv.s,this.hsv.v);},hueCursorClick: function(){this.hueCursorClickFlg = true;},colorSelectStart: function(){this.colorChangeFlg = true;this.hsv.s = Math.round((event.offsetX) * this.colorUnit);this.hsv.v = Math.round(Math.abs(((event.offsetY) * this.colorUnit)-100));this.rgb = this.hsv2rgb(this.hsv.h,this.hsv.s,this.hsv.v);},colorCursorClick: function(){this.colorCursorClickFlg = true;},dragPicker: function(){var cx = event.pageX;var cy = event.pageY;var container = document.getElementById('bigcolorpickercontainer');var colorArea = document.getElementById('bigcolorpickercolorarea');var conT = container.offsetTop;var conL = container.offsetLeft;if(this.hueChangeFlg){var hueArea = document.getElementById('bigcolorpickerhuepicker');var ht = hueArea.offsetTop + conT;var hh = hueArea.clientHeight;var hueCursorPos = cy - ht;if(hueCursorPos < 0) {hueCursorPos = 0;} else if(hueCursorPos > hh){hueCursorPos = hh;}this.hsv.h = Math.round(Math.abs((this.hueUnit * hueCursorPos)-360));this.rgb = this.hsv2rgb(this.hsv.h,this.hsv.s,this.hsv.v);}if(this.colorChangeFlg){var ct = colorArea.offsetTop + conT;var cl = colorArea.offsetLeft + conL;var ch = colorArea.clientHeight;var cw = colorArea.clientWidth;var colorCursorPosY = cy - ct;if(colorCursorPosY < 0) {colorCursorPosY = 0;} else if(colorCursorPosY > ch){colorCursorPosY = ch;}var colorCursorPosX = cx - cl;if(colorCursorPosX < 0) {colorCursorPosX = 0;} else if(colorCursorPosX > cw){colorCursorPosX = cw;}this.hsv.s = Math.round(colorCursorPosX * this.colorUnit);this.hsv.v = Math.round(Math.abs((colorCursorPosY * this.colorUnit)-100));this.rgb = this.hsv2rgb(this.hsv.h,this.hsv.s,this.hsv.v);}},selectEnd: function(){this.hueChangeFlg = false;this.colorChangeFlg = false;this.hueCursorClickFlg = false;},hexInput: function(){var hex = event.target.value;hex = hex.replace(/#/,'');if(!hex.match(/[A-Fa-f0-9]+/)){return false;} else {if(hex.length != 6 && hex.length != 3){return false;} else {if(hex.length == 3){hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);}}}this.rgb = this.hex2rgb(hex);this.hsv = this.rgb2hsv(this.rgb.r,this.rgb.g,this.rgb.b);},colorNumInput: function(type){if(event.type != 'keydown' && event.type != 'blur'){return false;} else {var val = event.target.value;if(!val.match(/\d+/)){return false;}val = Number(val);if(event.type == 'keydown'){if(event.key != 'Enter' && event.key != 'ArrowUp' && event.key != 'ArrowDown' && !event.shiftKey){return false;} else {var changeNum = 1;if(event.shiftKey){changeNum = 10;}if(event.key == 'ArrowUp'){val += changeNum;} else if(event.key == 'ArrowDown'){val -= changeNum;}}}if(type == 'r' || type == 'b' || type == 'g'){if(val > 255){val = 255;} else if(val < 0){val = 0;}if(type == 'r'){this.rgb.r = val;} else if(type == 'g'){this.rgb.g = val;} else if(type == 'b'){this.rgb.b = val;}this.hsv = this.rgb2hsv(this.rgb.r,this.rgb.g,this.rgb.b);} else if(type == 'a'){if(val > 100){val = 100;} else if(val < 0){val = 0;}this.alpha = (val / 100).toFixed(2);} else if(type == 'h' || type == 's' || type == 'v'){if(type == 'h'){if(val > 360){val = 360;} else if(val < 0){val = 0;}this.hsv.h = val;} else if(type == 's'){if(val > 100){val = 100;} else if(val < 0){val = 0;}this.hsv.s = val;} else if(type == 'v'){if(val > 100){val = 100;} else if(val < 0){val = 0;}this.hsv.v = val;}this.rgb = this.hsv2rgb(this.hsv.h,this.hsv.s,this.hsv.v);}}},rgbaCodeInput: function(event){var rgbaCode = event.target.value;rgbaCode = rgbaCode.replace(/rgba?/,'');rgbaCode = rgbaCode.replace(/\(/,'');rgbaCode = rgbaCode.replace(/\)/,'');rgbaCode = rgbaCode.replace(/\ /,'');var rgbaCodeAry = rgbaCode.split('\,');if(rgbaCodeAry.length < 3){return false;} else {if(!rgbaCodeAry[0].match(/\d+/) || !rgbaCodeAry[1].match(/\d+/) || !rgbaCodeAry[2].match(/\d+/) ){return false;}}this.rgb.r = rgbaCodeAry[0];this.rgb.g = rgbaCodeAry[1];this.rgb.b = rgbaCodeAry[2];this.alpha = rgbaCodeAry[3];this.hsv = this.rgb2hsv;},focusAndSelect: function(){event.target.select();},removeAll:function(){document.getElementById('bigcolorpicker-wrap').remove();},hex2rgb: function(hex){return {r:parseInt(hex.slice(0,2),16),g:parseInt(hex.slice(2,4),16),b:parseInt(hex.slice(4),16)};},rgb2hsv: function() {var rr, gg, bb,r = arguments[0] / 255,g = arguments[1] / 255,b = arguments[2] / 255,h, s,v = Math.max(r, g, b),diff = v - Math.min(r, g, b),diffc = function(c){return (v - c) / 6 / diff + 1 / 2;};if (diff == 0) {h = s = 0;} else {s = diff / v;rr = diffc(r);gg = diffc(g);bb = diffc(b);if (r === v) {h = bb - gg;}else if (g === v) {h = (1 / 3) + rr - bb;}else if (b === v) {h = (2 / 3) + gg - rr;}if (h < 0) {h += 1;}else if (h > 1) {h -= 1;}}if(this.hsv.h == 360 && h == 0){h = 1;}return {h: Math.round(h * 360),s: Math.round(s * 100),v: Math.round(v * 100)};},hsv2rgb: function(h,s,v){var max = v;var min = max - ((s / 255) * max);var rgb = {'r':0,'g':0,'b':0}; if (h == 360){h = 0;}s = s / 100;v = v / 100;if (s == 0){rgb.r = v * 255;rgb.g = v * 255;rgb.b = v * 255;return rgb;} var dh = Math.floor(h / 60);var p = v * (1 - s);var q = v * (1 - s * (h / 60 - dh));var t = v * (1 - s * (1 - (h / 60 - dh)));switch (dh){case 0 : rgb.r = v; rgb.g = t; rgb.b = p; break;case 1 : rgb.r = q; rgb.g = v; rgb.b = p; break;case 2 : rgb.r = p; rgb.g = v; rgb.b = t; break;case 3 : rgb.r = p; rgb.g = q; rgb.b = v; break;case 4 : rgb.r = t; rgb.g = p; rgb.b = v; break;case 5 : rgb.r = v; rgb.g = p; rgb.b = q; break;} rgb.r = Math.round(rgb.r * 255);rgb.g = Math.round(rgb.g * 255);rgb.b = Math.round(rgb.b * 255);return rgb; },},computed: {rgbaCode: function(){var alpha = this.alpha;if(alpha == 1.00){alpha = 1;} else if(alpha == 0.00){alpha = 0;}return 'rgba(' + this.rgb.r + ',' + this.rgb.g + ',' + this.rgb.b +',' + alpha + ')';},hueUnit: function(){return 360 / this.colorPickerSize},hueCursorPos: function(){return Math.round((Math.abs(this.hsv.h-360) / this.hueUnit) - this.hueCursorSize/2)},colorUnit: function(){return 100 / this.colorPickerSize},colorCursor: function(){return {left: (this.hsv.s / this.colorUnit) - this.colorCursorSize/2,top: Math.abs((this.hsv.v / this.colorUnit) - this.colorPickerSize) - this.colorCursorSize/2}},colorCursorState: function(){var color = "white";if(this.hsv.h <= 201 && this.hsv.h >= 22){if(this.hsv.v >= 50){color = "black";} else {color = "white";}} else {if(this.hsv.v >= 50 && this.hsv.s <=71){color = "black";} else {color = "white";}}return color;},alphaInt: function(){return Math.round(this.alpha * 100);},rgb2hex: function(){this.rgb.r = Math.round(this.rgb.r);this.rgb.g = Math.round(this.rgb.g);this.rgb.b = Math.round(this.rgb.b);return ('00' + this.rgb.r.toString(16)).slice(-2) + ('00' + this.rgb.g.toString(16)).slice(-2) + ('00' + this.rgb.b.toString(16)).slice(-2);},pickerStyle: function(){return {backgroundColor:'hsl(' + this.hsv.h + ',100%,50%)',opacity:this.alpha,width:this.colorPickerSize + 'px',height:this.colorPickerSize + 'px',}},containerStyle: function(){return {width:(this.colorPickerSize + 380) + 'px'}}},template:`<div><div id="bigcolorpickercontainer" class="bigcolorpickercontainer" @mouseup="selectEnd" @mouseleave="selectEnd" @mousemove="dragPicker" :style="containerStyle"><p class="title">Big Color Picker</p><div class="remove-btn" @click="removeAll">×</div><div class="inner"><div class="colorpicker-wrap"><div id="bigcolorpickercolorarea" class="colorpicker" @mousedown="colorSelectStart" :style="pickerStyle"><div :style="{left:this.colorCursor.left + 'px',top:this.colorCursor.top + 'px'}" class="colorpicker-cursor" :class="this.colorCursorState"></div></div></div><div id="bigcolorpickerhuepicker" class="huepicker" @mousedown="hueSelectStart" :style="{height:this.colorPickerSize + 'px'}"><div @mousedown="hueCursorClick" class="huepicker-cursor" :style="{top:this.hueCursorPos + 'px'}"></div></div><div class="preview_area"><div class="preview-wrap"><div class="preview" :style="{backgroundColor:'#' + this.rgb2hex,opacity:this.alpha}"></div></div><div class="colorcode-wrap"><div class="colorcode-boxes-wrap"><div class="colorcode-boxes"><div class="colorcode-box"><span>R</span><input type="text" :value="this.rgb.r" @keydown="colorNumInput('r')" @blur="colorNumInput('r')"></div><div class="colorcode-box"><span>G</span><input type="text" :value="this.rgb.g" @keydown="colorNumInput('g')" @blur="colorNumInput('g')"></div><div class="colorcode-box"><span>B</span><input type="text" :value="this.rgb.b" @keydown="colorNumInput('b')" @blur="colorNumInput('b')"></div><div class="colorcode-box"><span>A</span><input type="text" :value="this.alphaInt" @keydown="colorNumInput('a')" @blur="colorNumInput('a')"></div></div><div class="colorcode-boxes"><div class="colorcode-box"><span>H</span><input type="text" :value="this.hsv.h" @keydown="colorNumInput('h')" @blur="colorNumInput('h')"></div><div class="colorcode-box"><span>S</span><input type="text" :value="this.hsv.s" @keydown="colorNumInput('s')" @blur="colorNumInput('s')"></div><div class="colorcode-box"><span>B</span><input type="text" :value="this.hsv.v" @keydown="colorNumInput('v')" @blur="colorNumInput('v')"></div></div></div><input type="text" class="colorcode-rgb" :value="this.rgbaCode" @blur="rgbaCodeInput" @keyup.13="rgbaCodeInput" @focus="focusAndSelect"><input type="text" class="colorcode-hex" :value="'#' + this.rgb2hex" @blur="hexInput" @keyup.13="hexInput" @focus="focusAndSelect"></div></div></div></div></div>`});}); |
デザイナーにとってのVue.js / jQueryとの比較
◯ 複数の要素が相互に絡み合うような複雑な処理はjQueryよりもVue.jsの方が圧倒的に楽
- 例:入力フォームのインタラクションやリアルタイムバリデーション
- 例:複雑な条件分岐が必要な検索パネル
◯ 導入時の心理的ハードルが高いが、一度Vue.jsの作法を把握してしまうとむしろjQueryより難易度が低い
- 「書く場所が決まっている」ことがかなり嬉しい。DOMの制御とロジックを完全に分離して書けることによる心理的恩恵が想像以上に大きい。
- 「.js-◯◯◯」とか「.is-◯◯◯」みたいな慣例的テクニックによるクラス命名が必要ない。
△ ちょっとしたDOM操作であればjQueryで十分
- ボックスのアコーディオン展開とか、モーダル表示、軽いアニメーション程度の実装であればjQueryのほうが楽だし便利。
△ HTMLの管理・修正に混乱が発生する可能性がある
- HTML記述箇所が分散して、統一したHTMLソース管理に支障をきたすおそれ。
- Vue.jsの記法を理解していないとHTML修正のハードルが高い。
- BladeやSmartyなどのPHPテンプレートエンジン記法との競合・混乱に注意が必要
Vue.js導入によるメリット/デメリットはケースバイケースで、既存プロジェクトへの安易な導入はデザイナーへの負担が高そうです。
とはいえ、エンジニアが書いたVue.jsに手も足も出ない、という状況は避けるべきでしょうし、教養として学習しても損はないと感じました。
なにより面白いです。
最後に
この記事の執筆中、昨今のカラーピッカー事情はどうなっているだろう? と気になり、
Goolgleで「カラーピッカー」と検索した結果、こんなツールが表示されて度肝を抜かれました。
さすがです。とても高機能です。。
こんなものがあるのなら敢えて作る必要もなかったかもしれませんが、
ともあれとても勉強になりましたし、自分で作ったツールには愛着があります。
今回は本当に基礎的な機能しか利用しませんでしたが、
機会があればコンポーネントやルーティングを駆使したSPAの開発など携わってみたいものです。
とりとめのない長文にお付き合いいただき、ありがとうございました。
参考サイト
以下のCodeGridの記事は第2回目以降は有料ですが、丁寧かつ簡潔に解説されており、大変勉強になりました。