Google Apps Scriptで座席表を作ろう - イメージマップ編【GAS】

前回のお話、jQueryのライブラリを利用して、タイル状に配置した座席表を作りました。多くの事業所では、このような四角い配置で良いかと思いますが、実際には最近流行りのオシャレ事務所となると、机の配置から椅子の並びなどが結構フリースタイルな状況で、それに対応しようとすると、通常の手法では座席表を実現するのは困難になります。

今回、背景に事務所内座席レイアウトに対して、クリッカブルマップキャンバスを使って、座席表を実現してみたいと思います。

今回使用するスプレッドシートおよびソフトウェア

※2022年8月4日、より簡単なSVG画像を使ったレスポンシブ対応の座席表管理システムを作りました。

Google Apps ScriptでSVGを使ったイメージマップを操作 – 応用編【GAS】

概要

今回は背景の画像とキャンバスそしてクリッカブルマップを被せるjq_imgMapToCanvas2.jsというライブラリを使っています。レイヤ1に通常事務所座席レイアウト、レイヤ2にクリッカブルマップ(更にマウスオーバー時に色が変わる仕様)となるライブラリなのですが、キャンバスが一つですと、この色が変わる機能の為に、マウスオーバー後にそのままだと描写したキャンバスイメージが全部消えるため、座っている人の名前については、もうひとつキャンバスを被せて、そちらに描写する3枚サンド仕立てになっています。

下から背景画像、真ん中に座ってる人の名前(canvas1つ目)、その上にクリッカブルマップ(canvas2つ目)といった状況です。

クリッカブルマップとはHTML初期の頃からある「1つの画像上に座標を持って、複数のリンクを貼る技術」で、例えば1枚の地図にて、エリアごとにリンク先が異なるように座標でエリア分けしてリンクを貼れる技術です。今ではSVG画像などを用いるケースもあります。作るのが少々大変なので、この後の準備の項目では、それを容易にするサービスを利用してタグを生成します。

キャンバスとはHTML5を代表する技術で「HTML上にタグを持って画像を生成し、更にそれを操作する為の技術」で、アニメーションなどをさせる事も可能。画像と言ってもいわゆるPNGやJPGのような画像ファイルではなく、ベクターグラフィックスのようなものをタグで生成する事が出来ます。

キャンバスを重ねる時には、z-indexの値でそれぞれのキャンバスにレイヤ位置を持たせるのですが、マウスで直接的に操作出来るのは一番上のレイヤになります。重ねるとその下のレイヤは直接的には操作が出来ないので注意が必要です。

事前準備

画像を準備する

今回、ベースとなる800x500の枠付き画像を用意しておきました。この画像の上にお好きなペイントエディタなどを利用して、椅子や机などを配置して、フロアマップ画像を作成してください。業務で使ってるものは、1000x500のサイズで作っています。

画像形式はpngでもjpgでも可ですが、jpgの場合画質は落とすと掠れたりするので、100%画質で作ることをおすすめします。

図:こんな具合に作りました

クリッカブルマップを生成する

座席表データはXYの座標を持ったクリッカブルマップのHTMLコードが必要になります。といっても、タグ打ちでこれは人間では出来ません。そこで、これを容易にしてくれるHTML Imagemap Generatorを利用します。

以下の手順でフロアマップにクリッカブルマップを作り込みます。

  1. HTML Imagemap Generatorを開く
  2. フロアマップ画像をドラッグアンドドロップする。
  3. 四角形や丸、多角形の3種で椅子の部分を上手に囲む。丸の場合は、丸を囲むのではなく、丸を四角く囲む感じで作ります。
  4. 右サイドに生成されたHTMLコードがクリッカブルマップです。これを控えて置きます。
  5. Google Apps Script側にmapタグ内のareaタグデータをimagemap.htmlとして作って書き込んでおく

図:丸で椅子にクリッカブルマップを作ってる様子

図:imagemap.htmlを作成しておく

クリッカブルマップにIDを振る

生成されたareaタグをimagemap.htmlに貼り付けたら、各areaタグにIDを振ります。これは座席表シートのシートデータのID列と対応します。クリック時にこのIDを読み取ってスプレッドシート側のIDを見つけにいきます。areaタグ内に「id="park1"」といった具合に、タグを編集しておきましょう。

今回のサンプルは14席なので、スプレッドシート側もpark14まで作ってあります。

図:このIDはシートのレコード特定に重要な役目を果たします。

シートデータのXY座標を整備する

シートデータには、各IDにはラベルを表示する為のXY座標の数値を入れる必要があります。canvasのどの位置に表示するか?を決めるものですが、今回は椅子の丸の真上に表示するよう調整しています。クリッカブルマップのcoordsの値は(X座標, Y座標, 大きさ)を示していますので、これを利用します。

今回のケースでは、coordsのX座標 - 26した値をシートデータの座標Xの値として格納する。coordsのY座標 - 10した値をシートデータの座標Yの値として格納します。全てのIDのXY座標の値をこのような方法で埋めてください。

図:XY座標の値はラベルを表示する位置になります。

画像をアップロードして・・・

フロアマップ画像自体の画像はどこかに保存しておかなければなりません。其のためだけにサーバを用意してってのも勿体無いので、今回はGoogle Driveに画像をアップロードし、その画像を直接利用します。

  1. Google Driveに画像をアップロードする
  2. 画像のアクセス権限はG Suiteユーザならばドメインのユーザに許可をしておけば良いでしょう。今回はリンクを知ってる人全員に公開としています。
  3. 共有リンクを取得しましょう。
  4. Google Drive Direct Link Generatorを開きます。
  5. Enter Your sharing URLに3.で取得したリンクを貼り付けて、create direct linkをクリック
  6. Output Linkをコピーします。これは直接画像を開けるリンクです。これを控えておきましょう。

図:外部共有する場合には注意を払いましょう

図:フロアマップ画像への直リンクURLが取得できます。

ソースコード

今回、jq_imgMapToCanvas2.jsを利用し、クリッカブルマップにマウスオーバーで色の変わるライブラリをGoogle Apps Scriptの中で利用しています(imagemap2canvas.htmlに埋め込んでいます)。

jq_imgMapToCanvas2.jsについて

オリジナルのjq_imgMapToCanvas2.jsは、canvasを動的に生成するコードになっています。しかし、今回は座席ラベルの表示などをする関係上予め、HTML上にcanvasを作ってあるので、以下のようにコードを変更してあります。

function imgMapToCanvasInit(){
    $("img.imgMapToCanvas").each(function(){//すべてのimgMapToCanvas設定されたimg要素を処理
        //親(divやliなど)をrelative
        $(this).parent().css("position","relative");//div要素をposition:relativeに設定
        //イメージ属性
        var position=$(this).position();
        var height=$(this).attr("height");//img高さ=canvas高さ
        var width=$(this).attr("width");//img幅=canvas幅
        var src="url("+$(this).attr("src")+")";//img画像名=canvas背景
        var useMap=$(this).attr("usemap").split("#")[1];//usemap属性から#をとる
        var id="canvas_"+useMap;//上記をcanvasのidとする(canvas_***)

        //canvas設置
        var canvas = document.getElementById("canvas");//canvasはIDを指定する
        $(canvas).attr({"id":id,"width":width,"height":height});//canvasの幅・高さ設定
        $(this).before(canvas);//img要素の前にcanvasを設置
        if (!jQuery.support.opacity) {//IEの場合
            canvas = G_vmlCanvasManager.initElement(canvas);//IEの場合のcanvas初期化
        }

        //canvasに透明度1,z-indexを背面,positionを設定,背景にimg要素
        $("canvas#"+id)
                  .css({"opacity":"1.0","zIndex":"1","position":"absolute","top":position.top,"left":position.left,"backgroundImage":src});
        //img要素を透明度0.1(うっすらと),z-indexを前面,positionを設定
        $(this)
                  .css({"opacity":"0.1","zIndex":"5","position":"absolute","top":position.top,"left":position.left});
    });
}
  • canvas設置の部分、var canvasdocument.getElementById("canvas")として変更をしています。
  • また、クリッカブルマップのz-indexは5を設定。今回のアプリではもっとも上のレイヤになります。こうしておかないと、クリッカブルマップとホバー効果が無効になってしまいます(この上にさらにレイヤを置いてしまうと、そのレイヤに被さってしまうため)
  • ただしこのjq_imgMapToCanvas2.jsですが、electronでこのまま利用すると、エラーが出て動きません。そこでelectronで使いたい場合には、if (!jQuery.support.opacity) {}のセクションはコメントアウトするか?削除する必要があります。G_vmlCanvasManagerが原因で止まってしまいます。

GAS側コード

//起動時にパークエリアの状況をHTML側へ送る関数
function nowparkarea(){
  //シートIDを取得する
  var Prop = PropertiesService.getScriptProperties();
  var ssid = Prop.getProperty("sheetid")
  var ss = SpreadsheetApp.openById(ssid);

  //シートデータをガッツリ取得する
  var sheet = ss.getSheetByName("シートデータ").getRange("A2:F").getValues();

  //シートデータの塊を返す
  return JSON.stringify(sheet);

}

//深夜0時にデータをクリアするトリガー関数(パークエリア)
function clearpark(){
  //シートIDを取得する
  var Prop = PropertiesService.getScriptProperties();
  var ssid = Prop.getProperty("sheetid");
  var ss = SpreadsheetApp.openById(ssid);
  
  //シートデータを取得する
  var sdata = ss.getSheetByName("シートデータ").getRange("A2:D").getValues();
  var slength = sdata.length;
  
  //ループを回して固定フラグ判定をしながらデータ消去
  for(var i = 0;i<slength;i++){
    //フリーアドレスなのでクリアする
    sdata[i][2] = "";
    sdata[i][3] = "";
  }
  
  //配列データを一発貼り付け戻し
  var lastRow = sdata.length;      //レコードの数を取得する
  var lastColumn = sdata[0].length  //カラムの数を取得する
  ss.getSheetByName("シートデータ").getRange(2,1,lastRow,lastColumn).setValues(sdata);

}
  • シートの確保の為の関数と解放の為の関数は前回のライブラリ編と同じですので、今回は割愛しています。
  • nowparkareaではシートデータの座標データまで取得して含めて返しています。
  • clearpark関数は毎日0時にスクリプトトリガーによって、シートデータをクリアする関数です。

HTML側コード

<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- jQuery / jQuery UI -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
 
  <!-- jQuery Touch Punch - Enable Touch Drag and Drop -->
  <script src="https://officeforest.org/wp/library/shapeshift/jquery.touch-punch.min.js"></script>
  
  <!-- CSS -->
  <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
  <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
  
  <!-- imgMap2Canvasライブラリを読み込み -->
  <?!= HtmlService.createHtmlOutputFromFile("imagemap2canvas").getContent(); ?>
  
  <style>
    
    /* ダイアログの背景画像変更用 */
    .ui-widget-overlay {
      background: #000 url(https://officeforest.org/wp/library/forms/halloween.png) 10% 100% repeat-x;
      opacity: .50;
      background-size: 100% 100%;
      filter: Alpha(Opacity=70);
    }

    .box {
      float: left;
      width: 180px;
      text-align: center;
    }
    
    .box2 {
      float: left;
      width: 180px;
      text-align: center;
    }

    .boxContainer {
      overflow: hidden;
      width:100%;
    }
    
    /* clearfix */
    .boxContainer:before,
    .boxContainer:after {
      content: "";
      display: table;
    }
    
    .boxContainer:after {
      clear: both;
    }
    
    /* For IE 6/7 (trigger hasLayout) */
      .boxContainer {
      zoom: 1;
    }
    
    /* キャンバスを重ねる */
    #panel2{ 
     position: relative; 
     height: 502px; width: 1093px; //based on your canvas size 
    } 
    
    #canvas2{ 
     position: absolute; 
     top: 0; left: 0; opacity: 1; z-index: 4; top: 0px; left: 0px;
    } 
    
  </style>
  
  <!-- JavaScript -->
  <script>
    //クリック時に確保する一時座席ID
    var tempseat = "";

    //起動時に自動的に起動するスクリプト
    google.script.run.withSuccessHandler(onImagemap).getImagemap();

    //登録用ダイアログ表示設定
    $(function() {
        $( "#dialog" ).dialog({
          autoOpen: false,
          closeText: "保存せずに閉じます",
          width: 400,
          height: 200,
             title: "作業内容",
          modal: true,
          show: {
            effect: "clip",
            duration: 500
          },
          hide: {
            effect: "clip",
            duration: 500
          },
          position: {
            of : 'body',
            at: 'center',
            my: 'center'
          }
        });
    });	

    //登録用ダイアログ表示設定
    $(function() {
        $( "#dialog2" ).dialog({
          autoOpen: false,
          closeText: "保存せずに閉じます",
          width: 400,
          height: 200,
             title: "作業内容",
          modal: true,
          show: {
            effect: "clip",
            duration: 500
          },
          hide: {
            effect: "clip",
            duration: 500
          },
          position: {
            of : 'body',
            at: 'center',
            my: 'center'
          }
        });
    });	

    $(function() {
        $( "input[type=submit], a, button" )
          .button()
          .click(function() {
          });
    });

    //イメージマップ用HTMLを反映する
    function onImagemap(data){
      //HTMLテンプレートデータを反映
      document.getElementById("zasekimap").innerHTML = data;
      
      //パークエリアの要素クリック時の処理
      $(function(){
          $('area').on('click', function(e){
            //タグのIDを取得する
            var id =  $(this).attr("id");

            //一時変数に座席IDを確保
            tempseat = id;

            //ダイアログ表示
            $('#dialog').dialog({
                  title: "席の確保",
                  close : function(){
                  }
              });
            $( "#dialog" ).dialog( "open" );
            $( "#dialog" ).dialog("moveToTop");
            document.getElementById("dialog").focus();
          });
      });
      
      //imgmap2canvasの初期化
      $(function(){
          //初期化
          imgMapToCanvasInit();
          
          //クリッカブルマップ要素ホバー時の処理
          $("area").hover( //図形の形・座標取得→描画・消去 (ここから先thisはarea要素を示す)	
              function(e){//area:hover時
                  var id="canvas_"+$(this).parent().attr("name");//hoverになったareaのmapのname属性からidを取得
                  var shape=$(this).attr("shape").toLowerCase();//circleまたはpolyまたはrect、IE6は大文字取得なので小文字変換
                  var coords=$(this).attr("coords").split(",");//areaのcoords(座標)をカンマ区切りで配列に格納
                  for(i in coords){coords[i]=parseInt(coords[i]);}//文字列→数値
                                 //areaから色取得(area色情報無しの場合初期値)
                  var color=$(this).attr("color");//色取得
                  if(!color){color='rgba(255, 100, 0, 0.7)';}//areaに色指定していない場合の初期値
                  imgMapToCanvasDraw(id,shape,coords,color);//値をdrawする
              },
              function(e){//area:hover解除時にid指定→クリア
                  var id="canvas_"+$(this).parent().attr("name");//hoverになったareaのmapのname属性からidを取得
                  imgMapToCanvasClear(id);//値をclearする
              }
          );
      });
      
      //座席データをシートから取得し、反映する
      google.script.run.withSuccessHandler(onPark).nowparkarea();
    }
    
    //パークエリア状況を反映するコード
    function onPark(data){
      //データをパースする
      var json = JSON.parse(data);
      var length = json.length;
      
      //canvas2をクリアする
      var ctx = document.getElementById('canvas2').getContext('2d');
      ctx.clearRect(0, 0, 800, 500);
      
      //ループを回してシート状況を反映する
      for(var i = 0;i<length;i++){
        
        //固定フラグがONのデータの場合スルーする
        if(json[i][1] == true){
          //
          if(json[i][2] == null || json[i][2] == ""){
            //スルーする
          }else{
            //固定席ユーザを反映する
            canvastext(json[i][4],json[i][5],json[i][2]);
          }
          
        }else{
          //データが空の場合には、スルーし、ある場合には反映する
          if(json[i][2] == ""  || json[i][2] == null){
            //データが空なのでスルーする
            continue;
          }else{
            //シートデータを反映する
            canvastext(json[i][4],json[i][5],json[i][2]);
          }
        }
      }
    }

    //LocalStorageへのデータの挿入
    function setData(key, data){
      localStorage.setItem(key, data);
    }
    
    //LocalStorageからのデータの取得 
    function getData(key){
      var ret = localStorage.getItem(key);
      
      //null値判定
      if(ret == null){
        return "";
      }else{
        return ret;
      }
    }
    
    //シートをキープする処理(ダイアログのボタンクリック)
    function seatkeep(){
      //ダイアログ内のIDを取得する
      var manid = document.getElementById("wasabi").value;
      
      if(manid == ""){
        alert("社員IDが入っていませんよ");
        document.getElementById("wasabi").focus();
        return;
      }else{
        //ダイアログ内のIDをLocalStorageへ記憶する
        setData("mannum",manid);
      }
      
      //GAS側へシート確保処理をなげる
      google.script.run.withSuccessHandler(onGetSeat).getseatman(manid,tempseat);
      
      //ダイアログを閉じる
      document.getElementById("wasabi").value = "";
      $( "#dialog" ).dialog( "close" );

    }
    
    //シート確保をキャンセルする関数
    function seatcancel(){
      //ダイアログを閉じる
      document.getElementById("wasabi").value = "";
      $( "#dialog" ).dialog( "close" );
      
      //メッセージ表示
      alert("キャンセルされますた。");
    }
    
    //シート確保結果
    function onGetSeat(data){
      //返り値について取得する
      var json = JSON.parse(data);

      if(json[0] == true){
        //シートが確保出来た場合の処理
        alert("シートを確保しました。");
        
        //シートリストのリロード処理
        google.script.run.withSuccessHandler(onPark).nowparkarea();

      }else{
        //確保できなかった場合の処理
        alert(json[1]);
      }
    }
    
    //座席の解放ボタンを押した時のダイアログ表示用処理
    function releaseseat(){
      //シート確保用のダイアログを表示する
      //ダイアログ表示
      $('#dialog2').dialog({
        title: "席の解放",
        close : function(){
        }
      });
      
      $( "#dialog2" ).dialog( "open" );
      $( "#dialog2" ).dialog("moveToTop");
      document.getElementById("dialog2").focus();
      
      //IEに保存されてるIDを呼び出せるなら呼び出す
      document.getElementById("wasabi2").value = getData("mannum");    
    }
    
    //席の解放ボタンクリック時の処理
    function release(){
      //ダイアログ内のIDを取得する
      var manid = document.getElementById("wasabi2").value;
      
      if(manid == ""){
        alert("社員IDが入っていませんよ");
        document.getElementById("wasabi2").focus();
        return;
      }else{
        //ダイアログ内のIDをLocalStorageへ記憶する
        setData("mannum",manid);
      }
      
      //GAS側へシート確保処理をなげる
      google.script.run.withSuccessHandler(onRelSeat).relseatman(manid);
      
      //ダイアログを閉じる
      document.getElementById("wasabi2").value = "";
      $( "#dialog2" ).dialog( "close" );
    }
    
    //シート確保結果
    function onRelSeat(data){
      //返り値について取得する
      var json = JSON.parse(data);

      if(json[0] == true){
        //シートが確保出来た場合の処理
        alert("シートを解放しましたよ。");

        //パークエリアデータをリロード
        google.script.run.withSuccessHandler(onPark).nowparkarea();

      }else{
        //確保できなかった場合の処理
        alert(json[1]);
      }
    }    
 
    //シート確保をキャンセルする関数
    function seatcancel2(){
      //ダイアログを閉じる
      document.getElementById("wasabi2").value = "";
      $( "#dialog2" ).dialog( "close" );
      
      //メッセージ表示
      alert("キャンセルされますた。");
    }
   
    //canvasにテキストを描画する(canvas2レイヤに対して描画)
    function canvastext(xcoord,ycoord,text){
      //canvasを取得する
      var ctx = document.getElementById('canvas2').getContext('2d');
      var txw = ctx.measureText(text);
      
      //テキストのフォントのオプション指定
      ctx.font= 'bold 10px sans-serif';

      //背景ボックスを描画
      ctx.fillStyle = '#2023e5';
      ctx.fillRect(xcoord,ycoord,50,15);
      
      //枠の描画
      ctx.strokeStyle = '#2023e5';
      ctx.lineWidth = 5;
      ctx.strokeRect(xcoord,ycoord,50,15);
      
      //テキストを描画
      ctx.fillStyle = '#fff';
      ctx.textBaseline = 'top';
      ctx.fillText(text,xcoord,ycoord);
    }
    
  </script>
</head>
<body>
  <div style="width:800px;height:500px;border-color:'#000':border:3px">

    <div class="panel_area">
      <div id="panel2" class="tab_panel">
      <!-- パークエリアを描くキャンバス -->
      <div>
        <img class="imgMapToCanvas" usemap="#ImageMap" src="ここにフロアマップの画像のURLを入れる" width="800" height="500" border="0"/>
        </div>
          <map name="ImageMap" id="zasekimap"></map>
          <canvas class="canvas" width="800" height="500" id="canvas"></canvas>
          <canvas class="canvas" width="800" height="500" id="canvas2"></canvas>
      </div>
    </div>


    <hr>
    <div align="right">
      <button onClick='releaseseat()' id="releaseman" class="action" title='席をリリースします。'>座席の解放</button>
    </div>
    
    <div id="dialog" title="Basic dialog">
      <p>
      今日はこの席でお仕事しますか?座る場合は、IDを入れて、キープをクリックしてください。
      </p>
      <div align="center">
        <b>あなたのID:</b>&nbsp;&nbsp;<input type="text" id="wasabi" size="10" maxlength="6" placeholder="例:R91001">
      </div>
      
      <p></p>
      <div class='boxContainer'>
        <div class='box2'>
          <span><button onClick='seatkeep()' id="button1" style="font-size: 16px;vertical-align: middle" class="ponyo" title='席を確保します。'><img src='https://officeforest.org/wp/library/icons/icon_check2.png' />&nbsp; キープ</button></span>   
        </div>
        <div class='box'>
          <span><button onClick='seatcancel()' id="button2" style="font-size: 16px;vertical-align: middle" class="ponyo" title='キャンセルします。'><img src='https://officeforest.org/wp/library/icons/cross.png' />&nbsp; キャンセル</button></span>
        </div>
      </div>
    </div>
    
    <div id="dialog2" title="Basic dialog">
      <p>
      🍲今日はもうお帰りですか?座席を解放する場合は、IDを入れて、リリースをクリックしてください。
      </p>
      <div align="center">
        <b>あなたのID:</b>&nbsp;&nbsp;<input type="text" id="wasabi2" size="10" maxlength="6" placeholder="例:R91001">
      </div>
      
      <p></p>
      <div class='boxContainer'>
        <div class='box2'>
          <span><button onClick='release()' id="button3" style="font-size: 16px;vertical-align: middle" class="ponyo" title='席を解放します。'><img src='https://officeforest.org/wp/library/icons/icon_check2.png' />&nbsp; リリース</button></span>   
        </div>
        <div class='box'>
          <span><button onClick='seatcancel2()' id="button4" style="font-size: 16px;vertical-align: middle" class="ponyo" title='キャンセルします。'><img src='https://officeforest.org/wp/library/icons/cross.png' />&nbsp; キャンセル</button></span>
        </div>
      </div>
    </div>

  </div>
</body>
</html>
  • 起動時にシートデータを取得して、onImagemap関数でIDがcanvas2のタグに確保済みユーザのラベルを表示しています。
  • 背景画像およびクリッカブルマップ自体はIDがcanvasのタグに描画しています。IDがcanvasはz-indexが5番目のレイヤになるので、ここではもっとも上のレイヤとしています。
  • 座席確保したユーザのラベルはz-indexが4番目となるように、CSSにてIDがcanvas2のレイヤはz-index:4を指定しています。
  • 座席確保のダイアログ、座席解放のダイアログをそれぞれ用意しています。
  • 座席開放時はデータをクリアした後、再度データを読み込ませています。canvasは一度リセットするためにclearrectで指定した座標のエリアをリセットしています。
  • 今回画像が800x500のサイズで作っている為、全体のdiv、背景画像、canvasのそれぞれのサイズも800x500で指定しています。
  • panel2のIDを持つdivはホバーやクリッカブルマップを反映する親divとなります。子がIDがcanvasのdivとなります。
  • canvastextがcanvas2にユーザ名のラベルを反映する為の関数となります。

使い方と実行

使い方

このアプリの使い方ですが、

  1. スプレッドシート上のメニューより「座席表Plus」⇒「セットアップ」を実行する
  2. ユーザ表をきちんと整備しておく。ユーザ表のIDにないIDを入力されてもシートが確保されないようになります。
  3. スプレッドシート上のメニューより「リセットトリガー設置」を実行すると、毎日0時に座席データがクリアされるようになります。

実行結果

  • 丸をクリックするとダイアログが表示され、ユーザ表にあるIDと一致するIDを入力するとシートが確保されます。
  • 座席の解放をクリックすると、確保済みシートが解放され、canvasがリロードされます。
  • 丸はマウスオーバーで色が変わります。
  • トリガーを設置しておくと、毎日0時にシートデータがクリアされリセットされます。

高解像度対応で文字の滲みをなくす

低解像度なPCだとあまり気にならなかった今回のイメージマップ版座席表ですが、最近のPCやRetina Displayなモニターだと文字の滲み(アンチエイリアスなのかどうかわからないけれど)が結構気になるレベル。やはり文字がはっきり見えたほうが良い!!ということで、改造しました。

今回の改造ポイントは

  1. 文字表示を行うcanvasは別レイヤとして追加する(canvas3とする)
  2. 文字ははっきりクッキリきれいなフォントで見えるようにする
  3. 文字のバックにあるボックスは文字サイズに合わせたボックスの幅に自動調整する

これを実現するテクニックは

  • canvas内で指定するheightとwidthは実際に表示するサイズの2倍サイズを指定する(今回下地の画像が800x500のサイズなので、1600x1000で指定をしています。
  • 同時にCSSでそのcanvasのheightとwidthは実際の800x500の指定をするというのがポイントになります。
  • canvasの初期化の時点でcontextのscaleを二倍サイズにするコードを入れておく
  • 背景のボックスは文字幅をmeasureTextで取得してそのサイズを指定してあげるようにする

これらをコードにすると

#canvas3{
  position: absolute; 
  top: 0; left: 0; opacity: 1; z-index: 3;   
  width:800px;
  height:500px;
}

コード:canvas3のサイズを指定するCSS

//canvas3のサイズを調整
$(function() {
   var ctx = document.getElementById('canvas3').getContext('2d');
   ctx.scale(2,2);
});

コード:canvas3の初期化時にスケール2倍を指定

//canvasにテキストを描画する(canvas3レイヤに対して描画)
function canvastext(xcoord,ycoord,text){
      // CanvasとContextを取得する。
      var canvas = document.getElementById("canvas3");
      var ctx    = canvas.getContext("2d");
      
      //テキストのフォントのオプション指定
      ctx.font= 'bold 14px sans-serif';
      
      //テキストを描画
      var txw2 = ctx.measureText(text);
   
      //テキストサイズにマッチしたボックスを描画する
      ctx.fillStyle = '#008C22';
      ctx.fillRect(xcoord,ycoord-10,txw2.width,20);
      
      //枠を描画する
      ctx.strokeStyle = '#008C22';
      ctx.lineWidth = 5;
      ctx.strokeRect(xcoord,ycoord-10,txw2.width,20);
      
      //テキストデータを書き込む
      ctx.fillStyle = '#fff';
      ctx.textBaseline = 'middle';
      ctx.fillText(text,xcoord,ycoord);

}

※OnParkのcanvasも文字データをクリアするコードもcanvas2ではなくcanvas3にしておくのを忘れずに。

コード:canvas3に文字を描画する関数

 <canvas id="canvas3" width=1600 height=1000></canvas>

※canvasに他のcanvas同様のclass="canvas"をつけておくと、文字が横に伸びてしまうので、canvas3では余計なclass指定は外しておきましょう。

コード:HTML側のcanvas3の状態

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)