GoogleSpreadSheetで共同編集可能ドット絵エディタを作ろう！

#作ったもの
https://docs.google.com/spreadsheets/d/1c9zfSnBAPakqEkAouGaCVpPSVj_C3AnVhn8OZC_bxNY/edit?usp=sharing

#なぜ作ったか
その昔、Excelでドット絵エディタ的なものを作ってみた事があったので、GoogleSpreadSheet(GAS)でも作れるのでは？
と思い軽い気持ちで手を出しました。
が、そこには思った以上の罠が待ち構えていたのです。

#機能要件
- ドットが置ける
- パレットから色が取得できる
- 既に置かれているドットからも色が取得できる

ぐらいあれば、まぁドット絵ツールとして認められるんじゃないでしょうか。

#画面構成
まず、~~みんな大好き~~方眼紙を作ります。
そこにパレット用のエリアと、お絵かき用のエリアを作成。
パレット用のエリアには適当にプリセットとして色を配置しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/56609677-1488-8335-d746-ca18b0b5cc59.png)

おお。もう大分それっぽいですね。

これで、パレットエリアをクリックしたら色を保持して、お絵かきエリアをクリックしたらその色を置くようにできれば完成です。
なんだ楽勝じゃないか。

もちろんそんなことは無いのですが。

#イベントが無い！
あれ。無いぞ・・・。　ドキュメントや記事を、いくら探しても探しても・・・無い！。　
そうなのです。　GoogleSpreadSheet（GAS)にちょっと手を出したことのある人には常識なのかもしれませんが、なんと**クリックイベント**が無い！
そして、セルのSelectionChange的なものも無い！(実はあるらしいのだけれど、なぜかまともに動かない）

これでは、到底

- クリックして色を取る

も

- クリックしてドットを置く

も出来そうもありません。

考え方を変える必要がありそうです。

#条件付き書式
あの手この手を考えたり、検索してたりすると、僕は知らなかったんですが、GoogleSpreadSheetにも**条件付き書式**があるのを発見！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/cc547a7b-73a4-a8a0-a2c9-cb4d2104f6cc.png)

これをうまく使えば・・・？

①カラーパレットの色一つ一つに（仮に0~63の数字）文字列を割り振る
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/f54ff60a-4832-fd52-c25c-4375cc8d2271.png)

②お絵描きエリアにその色に対応した文字列を置く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/d917005d-9f3b-ff1e-2e81-51bcbe53c887.png)


③条件付き書式 の機能により 文字列に対応した色が付く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/4240f48f-30cd-064e-bd9f-9fa545a777d6.png)



が出来るのでは！？

ということで、やってみました。

#コード
この**条件付き書式**はもちろんGASから以下のように設定する事が出来ます。

```js
      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個)設定してあげればいいわけです。これは勝った。

まず、パレットエリアと、お絵かきエリアの範囲を定義しておきます

```js
var Const = {
  palletRangeFormula :'A2:H9',
  paintRangeFormula : 'J2:AO34',
};
```

次に、パレットエリアの文字と背景色に応じて、お絵かきエリアに条件付き書式を設定する処理を書いていきます

```js
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);
};
```

作り終わってみると、なんてことはない、非常に簡単な処理ですね（ツライ

実際動かすと、こんな感じになります。　ちょっとだけラグがありますね・・・。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">GoogleSpreadSheet(+GAS)で、ドット絵エディタ作りました。 <a href="https://t.co/at1EfCqUSJ">pic.twitter.com/at1EfCqUSJ</a></p>&mdash; すずきかつーき (@divideby_zero) <a href="https://twitter.com/divideby_zero/status/1282885422230597633?ref_src=twsrc%5Etfw">July 14, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#パレット変更機能
完成！！と思いきや、一つ問題があります。
それは、このカラーパレット適用処理(条件付き書式設定処理)はいつ、だれが呼ぶのか。　と言う話です。

シートのオープン時に呼ぶのはまぁ、良いのですが、途中でカラーパレットを変更した場合は？

ちょっと調べるとGoogleSpreadSheetに用意されている（少ない)イベント処理の中に `onEdit(e)` と言うものが見つかります。
これを使えば・・・？　と思うわけなんですが、実はこの`onEdit(e)`は、**値の変更**には反応しますが、**背景色の変更**などでは発火しないという仕様（？）があります。

かならず対応する文字も変更してくれる。　と言うのであれば、`onEdit(e)` でカラーパレットの範囲の変更を検知して、上記の`setPallet()` を呼べばよいのですが、そうとも限らず・・・。

もっともっと良く調べると、**トリガー**を別途指定することで背景色の変更なども検知できることがわかりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/ff0ecf5d-a4d6-4357-7d92-e73cbdc27aad.png)

ここでは、`onChange` というfunctionを呼び出しており、`onChange` は中では`setPallet()`を呼び出すだけです。

```js
function onChange(){
  setPallet();
}
```

こうすることで、色変更をするだけでもsetPallet(条件付き書式設定処理)が呼ばれるので、既に書いたお絵描きエリアの色も一括で変更されるようになりました。やったね！！！（？）

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">カラーパレットを変更する機能も付けました（無理やり） <a href="https://t.co/mHvXwC3UGF">pic.twitter.com/mHvXwC3UGF</a></p>&mdash; すずきかつーき (@divideby_zero) <a href="https://twitter.com/divideby_zero/status/1282887593374314497?ref_src=twsrc%5Etfw">July 14, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#やったね！！？
しかし、実際のところこの処理は全然いけてません。
トリガーは「実行数」というのも見る事ができるのですが
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/645640f5-f32e-d586-b83a-30a1243f5789.png)
めっちゃ呼ばれてます。
しかも、毎回1秒近くかかってます。

これは、setPalletが重い処理だという事もあるのですが、このトリガーを使った変更検出は、**どこを変更したか**までは教えてくれないからです。

本来なら、**「カラーパレットの領域が編集されていたら、setPalletを実行」**という処理にしたいのですが、それが出来ない。
逆に、`onEdit(e)` は渡された `e` に編集箇所や、編集前の値など細かく入っているので制御が可能、だけれど背景色の変更では発火しない。

あちらを立てればこちらが立たず。非常～～～～に悩ましい。

もういっそ、パレットの変更はユーザーからやってもらおうか。　と、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/af0d9f9d-4900-184f-1260-c619a49601d6.png)

メニューにこんなのも追加してみましたが、やっぱり自動でパレット変更された方がかっこいいですよね。

こちら、なにかいい手をご存じの方がおられましたら、コメント等で教えていただけるとありがたいです。
それでは。

#おまけ（ソースコード全部)
```js:コード.gs
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();  
}

//パレットに０～６３の数字を割り当てる
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);
};
```

```js:マクロ.gs
//シート追加 テンプレートをコピー
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);
};
```