作ったもの
なぜ作ったか
その昔、Excelでドット絵エディタ的なものを作ってみた事があったので、GoogleSpreadSheet(GAS)でも作れるのでは?
と思い軽い気持ちで手を出しました。
が、そこには思った以上の罠が待ち構えていたのです。
機能要件
- ドットが置ける
- パレットから色が取得できる
- 既に置かれているドットからも色が取得できる
ぐらいあれば、まぁドット絵ツールとして認められるんじゃないでしょうか。
画面構成
まず、みんな大好き方眼紙を作ります。
そこにパレット用のエリアと、お絵かき用のエリアを作成。
パレット用のエリアには適当にプリセットとして色を配置しておきます。
おお。もう大分それっぽいですね。
これで、パレットエリアをクリックしたら色を保持して、お絵かきエリアをクリックしたらその色を置くようにできれば完成です。
なんだ楽勝じゃないか。
もちろんそんなことは無いのですが。
イベントが無い!
あれ。無いぞ・・・。 ドキュメントや記事を、いくら探しても探しても・・・無い!。
そうなのです。 GoogleSpreadSheet(GAS)にちょっと手を出したことのある人には常識なのかもしれませんが、なんとクリックイベントが無い!
そして、セルのSelectionChange的なものも無い!(実はあるらしいのだけれど、なぜかまともに動かない)
これでは、到底
- クリックして色を取る
も
- クリックしてドットを置く
も出来そうもありません。
考え方を変える必要がありそうです。
条件付き書式
あの手この手を考えたり、検索してたりすると、僕は知らなかったんですが、GoogleSpreadSheetにも条件付き書式があるのを発見!
これをうまく使えば・・・?
①カラーパレットの色一つ一つに(仮に0~63の数字)文字列を割り振る
が出来るのでは!?
ということで、やってみました。
コード
この条件付き書式はもちろんGASから以下のように設定する事が出来ます。
var conditionalFormatRules = []; //条件付き書式配列を作成
var rule = SpreadsheetApp.newConditionalFormatRule(); //条件付き書式を新規作成
conditionalFormatRules.push( //配列に追加
rule.setRanges([targetRange]) //ルールを適用する範囲(Range)
.whenTextEqualTo(text) //今回の場合は文字列一致したら
.setBackground(bgcolor) //背景色('#ff0000'等)
.setFontColor(bgcolor) //文字色(背景色と同じにしておくことで、文字が見えなくさせる)
.build() //構築
);
spreadsheet.getActiveSheet().setConditionalFormatRules(conditionalFormatRules); //作った条件付き書式配列をアクティブシートにセット
なので、このrule
を作成して、conditionalFormatRules
に追加するところを、カラーパレット数分(今回だと64個)設定してあげればいいわけです。これは勝った。
まず、パレットエリアと、お絵かきエリアの範囲を定義しておきます
var Const = {
palletRangeFormula :'A2:H9',
paintRangeFormula : 'J2:AO34',
};
次に、パレットエリアの文字と背景色に応じて、お絵かきエリアに条件付き書式を設定する処理を書いていきます
function setPallet() {
var spreadsheet = SpreadsheetApp.getActive();
var targetRange = spreadsheet.getRange(Const.paintRangeFormula); //お絵描きエリアのRange->条件付き書式を適用する範囲
var palletRange = spreadsheet.getRange(Const.palletRangeFormula); //カラーパレットのRange
var conditionalFormatRules = [];
//カラーパレットの範囲を全部for文で回し、書かれている文字と背景色から条件付き書式配列を作成していく
var rowEnd = palletRange.getHeight();
var columnEnd = palletRange.getWidth();
for (var r = 1; r <= rowEnd; r++) {
for (var c = 1; c <= columnEnd; c++) {
var bgcolor = palletRange.getCell(r, c).getBackground(); //背景色取得
var text = palletRange.getCell(r,c).getValue(); //対応する文字列取得
var rule = SpreadsheetApp.newConditionalFormatRule();
if(bgcolor == '#ffffff')continue; //白は要らない。Deleteで文字を消せばいいので。
conditionalFormatRules.push(
rule.setRanges([targetRange])
.whenTextEqualTo(text)
.setBackground(bgcolor)
.setFontColor(bgcolor)
.build());
}
}
spreadsheet.getActiveSheet().setConditionalFormatRules(conditionalFormatRules);
};
作り終わってみると、なんてことはない、非常に簡単な処理ですね(ツライ
実際動かすと、こんな感じになります。 ちょっとだけラグがありますね・・・。
GoogleSpreadSheet(+GAS)で、ドット絵エディタ作りました。 pic.twitter.com/at1EfCqUSJ
— すずきかつーき (@divideby_zero) July 14, 2020
パレット変更機能
完成!!と思いきや、一つ問題があります。
それは、このカラーパレット適用処理(条件付き書式設定処理)はいつ、だれが呼ぶのか。 と言う話です。
シートのオープン時に呼ぶのはまぁ、良いのですが、途中でカラーパレットを変更した場合は?
ちょっと調べるとGoogleSpreadSheetに用意されている(少ない)イベント処理の中に onEdit(e)
と言うものが見つかります。
これを使えば・・・? と思うわけなんですが、実はこのonEdit(e)
は、値の変更には反応しますが、背景色の変更などでは発火しないという仕様(?)があります。
かならず対応する文字も変更してくれる。 と言うのであれば、onEdit(e)
でカラーパレットの範囲の変更を検知して、上記のsetPallet()
を呼べばよいのですが、そうとも限らず・・・。
もっともっと良く調べると、トリガーを別途指定することで背景色の変更なども検知できることがわかりました。
ここでは、onChange
というfunctionを呼び出しており、onChange
は中ではsetPallet()
を呼び出すだけです。
function onChange(){
setPallet();
}
こうすることで、色変更をするだけでもsetPallet(条件付き書式設定処理)が呼ばれるので、既に書いたお絵描きエリアの色も一括で変更されるようになりました。やったね!!!(?)
カラーパレットを変更する機能も付けました(無理やり) pic.twitter.com/mHvXwC3UGF
— すずきかつーき (@divideby_zero) July 14, 2020
やったね!!?
しかし、実際のところこの処理は全然いけてません。
トリガーは「実行数」というのも見る事ができるのですが
めっちゃ呼ばれてます。
しかも、毎回1秒近くかかってます。
これは、setPalletが重い処理だという事もあるのですが、このトリガーを使った変更検出は、どこを変更したかまでは教えてくれないからです。
本来なら、**「カラーパレットの領域が編集されていたら、setPalletを実行」**という処理にしたいのですが、それが出来ない。
逆に、onEdit(e)
は渡された e
に編集箇所や、編集前の値など細かく入っているので制御が可能、だけれど背景色の変更では発火しない。
あちらを立てればこちらが立たず。非常~~~~に悩ましい。
もういっそ、パレットの変更はユーザーからやってもらおうか。 と、
メニューにこんなのも追加してみましたが、やっぱり自動でパレット変更された方がかっこいいですよね。
こちら、なにかいい手をご存じの方がおられましたら、コメント等で教えていただけるとありがたいです。
それでは。
おまけ(ソースコード全部)
var Const = {
palletRangeFormula :'A2:H9',
paintRangeFormula : 'J2:AO34',
};
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('ドットエディタ')
.addItem('パレット変更', 'setPallet')
.addSeparator()
.addItem('初期化', 'Clear')
.addSeparator()
.addItem('新規シート追加', 'newSheet')
.addToUi();
}
function onChange(){
setPallet();
}
function onEdit(e) {
var sheet = e.source.getActiveSheet();
var rowStart = e.range.rowStart;
var rowEnd = e.range.rowEnd;
var columnStart = e.range.columnStart;
var columnEnd = e.range.columnEnd;
var palletRange = sheet.getRange(Const.palletRangeFormula);
if(rowStart < palletRange.getRow())return;
if(columnStart < palletRange.getColumn())return;
if(rowStart > palletRange.getRow() + palletRange.getHeight())return;
if(columnStart > palletRange.getColumn() + palletRange.getWidth())return;
setPallet();
}
//パレットに0~63の数字を割り当てる
function palletInitialize(){
var spreadsheet = SpreadsheetApp.getActive();
var palletRange = spreadsheet.getRange(Const.palletRangeFormula);
var values = palletRange.getValues();
for(var ix = 0;ix < values.length;ix++){
for(var iy = 0;iy < values[ix].length;iy++){
values[iy][ix] = iy * values.length + ix;
}
}
palletRange.setValues(values);
}
function setPallet() {
var spreadsheet = SpreadsheetApp.getActive();
var targetRange = spreadsheet.getRange(Const.paintRangeFormula);
var palletRange = spreadsheet.getRange(Const.palletRangeFormula);
var rowEnd = palletRange.getHeight();
var columnEnd = palletRange.getWidth();
var conditionalFormatRules = [];
for (var r = 1; r <= rowEnd; r++) {
for (var c = 1; c <= columnEnd; c++) {
var bgcolor = palletRange.getCell(r, c).getBackground();
var text = palletRange.getCell(r,c).getValue();
var rule = SpreadsheetApp.newConditionalFormatRule();
if(bgcolor == '#ffffff')continue;
conditionalFormatRules.push(
rule.setRanges([targetRange])
.whenTextEqualTo(text)
.setBackground(bgcolor)
.setFontColor(bgcolor)
.build());
}
}
spreadsheet.getActiveSheet().setConditionalFormatRules(conditionalFormatRules);
};
//シート追加 テンプレートをコピー
function newSheet() {
var spreadsheet = SpreadsheetApp.getActive();
var templateSheet = spreadsheet.getSheetByName("テンプレート");
var newSheet = templateSheet.copyTo(spreadsheet);
var d = new Date();
var dt = Utilities.formatDate( d, 'Asia/Tokyo', 'yyyyMMdd_HHmmSS');
newSheet.setName('NewSheet_'+dt);
spreadsheet.setActiveSheet(newSheet);
spreadsheet.moveActiveSheet(1);
};
//現在のシートをテンプレートで上書き
function Clear() {
var spreadsheet = SpreadsheetApp.getActive();
var targetSheet = spreadsheet.getActiveSheet();
spreadsheet.getRange('A1').activate();
var templateSheet = spreadsheet.getSheetByName("テンプレート");
templateSheet.getRange(1, 1, templateSheet.getMaxRows(), templateSheet.getMaxColumns()).copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_NORMAL, false);
templateSheet.getRange(1, 1, templateSheet.getMaxRows(), templateSheet.getMaxColumns()).copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_COLUMN_WIDTHS, false);
targetSheet.setRowHeights(1, 40, 21);
};