質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.00%

canvasでの戻る機能が実装できません

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 1,670

kihokutarou

score 22

タブレットでの使用目的で、「自由に字や絵が描ける」「画像を呼び出してそれに絵画できる」「あらかじめおいておいたカードなどを動かせる(本件とは無関係)」などのプログラムをHTMLで書いています。

上記やりたいことはできるようになったのですが、絵画をした際、「戻る」機能の実装ができません。おそらくはその時点でのcanvasの情報を5個程度保存し、それを「戻る」プッシュで呼び出す、という感じだと思うのですが、素人なりに様々試しましたがまったくうまくいきません。どなたかご教示ください。
5回~10回程度の過去情報に「戻る」ができればよいと思います。

同階層に
imgフォルダ(動かすカードなどを格納)
a.jpg 初期状態のcanvasの背景画像
jquery.min.js
jquery.ui.touch-punch.js
jquery-ui-1.8.12.custom.min.js
move.js(動かす用のjs)
wboard.css
index.html(下記)
wboard.js(下記)
と入れております。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>汎用お絵かき試作品</title>
    <link rel="stylesheet" type="text/css" href="wboard.css">
</head>
<body>

<div class="wrapper">
<form>
<p>
&nbsp;&nbsp;色&nbsp;
<input type="button" class="b1" style="background: #000000;" value=" " onclick="change_black()">
<input type="button" class="b1" style="background: #ff0000;" value=" " onclick="change_red()">
<input type="button" class="b1" style="background: #00ff00;" value=" " onclick="change_green()">
<input type="button" class="b1" style="background: #0000ff;" value=" " onclick="change_blue()">
<input type="button" class="b1" style="background: #ffffff;" value=" " onclick="change_white()">
&nbsp;&nbsp;線幅&nbsp;
<input type="button" class="b2" value="8px" onclick="change_8()">
<input type="button" class="b2" value="16px" onclick="change_16()">
&nbsp;&nbsp;
<input type="button" class="b2" value="全面" onclick="spread()">
<input type="button" class="b2" value="消去" onclick="clr()">
<font size="5" color="#ff0000"><a href="javascript:location.reload();">最初から</a></font>
&nbsp;&nbsp;画像&nbsp;
<input id="ufile" name="ufile" type="file" accept="image/jpeg,image/png">
<input type="button" value="戻る" id="backBtn">
<input type="button" value="進む" id="forwardBtn">
</form>


<canvas id="drawArea"></canvas>



<p><font size="6" color="#ff0000">
<a href="#" onClick="toggle_view1();return false;">カードの表示</a>
<a href="#" onClick="toggle_hidden1();return false;">カードの非表示</a>
</font>
</p>
<div id="card"  style="display: none;">
<table><tr>
<td><p id="move1"><img src="img/1.JPG" width="150" height="120"></p></td>

<td><p id="move2"><img src="img/2.JPG" width="150" height="120"></p></td>

<td><p id="move3"><img src="img/3.JPG" width="150" height="120"></p></td>

<td><p id="move4"><img src="img/4.JPG" width="150" height="120"></p></td>

<td><p id="move5"><img src="img/5.JPG" width="150" height="120"></p></td>

<td><p id="move6"><img src="img/6.JPG" width="150" height="120"></p></td>

<td><p id="move7"><img src="img/7.JPG" width="150" height="120"></p></td>

<td><p id="move8"><img src="img/8.png" width="80" height="80"></p></td>

</tr></table>
</div>



<script src="wboard.js"></script>
<script src="jquery.min.js"></script>
<script type="text/javascript" src="jquery-ui-1.8.12.custom.min.js"></script>
<script type="text/javascript" src="jquery.ui.touch-punch.js"></script>
<script type="text/javascript" src="move.js"></script>

 <script>
         $(function(){

             // id="ufile"の変化でコールバック
             $("#ufile").change(function(){
                 // 選択ファイルの有無をチェック
                 if (!this.files.length) {
                     alert('ファイルが選択されていません');
                     return;
                 }

                 // Formからファイルを取得
                 var file = this.files[0];

                 // (1) HTMLのCanvas要素の取得
                 var canvas = $("#drawArea");

                 // (2) getContext()メソッドで描画機能を有効にする
                 var ctx = canvas[0].getContext('2d');

                 // 描画イメージインスタンス化
                 var image = new Image();

                 // File API FileReader Objectでローカルファイルにアクセス
                 var fr = new FileReader();

                 // ファイル読み込み読み込み完了後に実行 [非同期処理]
                 fr.onload = function(evt) {

                     // 画像がロードされた後にcanvasに描画を行う [非同期処理]
                     image.onload = function() {
                         // (3) プレビュー(Cnavas)のサイズを指定
                         var cnvsH = ctx.canvas.height;
                         var cnvsW = image.naturalWidth*cnvsH/image.naturalHeight;
                         // (4) Cnavasにサイズアトリビュートを設定する
                         canvas.attr('width', cnvsW);
                         canvas.attr('height', cnvsH);
                         // (5) 描画
                         ctx.drawImage(image, 0, 0, cnvsW, cnvsH);
                     }
                     // 読み込んだ画像をimageのソースに設定
                     image.src = evt.target.result;
                 }

                 // fileを読み込む データはBase64エンコードされる
                 fr.readAsDataURL(file);
             })
        })
</script>
<script language="JavaScript" type="text/javascript">
<!--
var elem1 = document.getElementById("card");
function toggle_view1() {
  elem1.style.display = "";
}
function toggle_hidden1() {
  elem1.style.display = "none";
}
-->
</script>
</body>
</html>
onload = function() {
  draw();
};
function draw() {
  var canvas = document.getElementById("drawArea");
  if ( ! canvas || ! canvas.getContext ) { return false; }
  var ctx = canvas.getContext("2d");
  /* Imageオブジェクトを生成 */
  var img = new Image();
  img.src = "a.jpg?" + new Date().getTime();
  /* 画像が読み込まれるのを待ってから処理を続行 */
  img.onload = function() {
    ctx.drawImage(img, 0, 0,ctx.canvas.width, ctx.canvas.height);
  }
}

var canvas = document.getElementById("drawArea");
var ctx = canvas.getContext("2d");
var width = window.innerWidth;
var height = window.innerHeight;
var x = 0, y = 0;
var color = "#000000";
var line = 13;
canvas.width = width;
canvas.height = height - 70;

canvas.addEventListener("mousedown", touchStartHandler, false);
canvas.addEventListener("mouseup", touchEndHandler, false);

canvas.addEventListener("touchstart", ttouchStartHandler, false);
canvas.addEventListener("touchend", ttouchEndHandler, false);

function touchStartHandler(e) {
    e.preventDefault();
    getTouchPoint(e);
    ctx.beginPath();
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.moveTo(x, y);
    canvas.addEventListener("mousemove", touchMoveHandler, false);
}

function touchMoveHandler(e) {
    e.preventDefault();
    getTouchPoint(e);
    ctx.lineWidth = line; //線の太さ
    ctx.strokeStyle = color; //線の色
    ctx.lineTo(x, y);
    ctx.stroke();
}

function touchEndHandler(e) {
    e.preventDefault();
    ctx.closePath();
    canvas.removeEventListener("mousemove", touchMoveHandler, false);
}

function getTouchPoint(e) {
    x = e.clientX - canvas.offsetLeft;
    y = e.clientY - canvas.offsetTop;
}

function ttouchStartHandler(e) {
    e.preventDefault();
    getTTouchPoint(e);
    ctx.beginPath();
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.moveTo(x, y);
    canvas.addEventListener("touchmove", ttouchMoveHandler, false);
}

function ttouchMoveHandler(e) {
    e.preventDefault();
    getTTouchPoint(e);
    ctx.lineWidth = line; //線の太さ
    ctx.strokeStyle = color; //線の色
    ctx.lineTo(x, y);
    ctx.stroke();
}

function ttouchEndHandler(e) {
    e.preventDefault();
    ctx.closePath();
    canvas.removeEventListener("touchmove", ttouchMoveHandler, false);
}

function getTTouchPoint(e) {
    var touch = e.touches[0];
    x = touch.pageX - canvas.offsetLeft;
    y = touch.pageY - canvas.offsetTop;
}

function spread() {
    ctx.fillStyle = color;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    document.getElementById("ClickSound").play();
}

function clr() {
    cntx = canvas.getContext("2d");
    cntx.clearRect(0, 0, canvas.width, canvas.height);
    document.getElementById("ClickSound").play();
}

function change_black() {
    color = "#000000";
    document.getElementById("ClickSound").play();
}
function change_red() {
    color = "#ff0000";
    document.getElementById("ClickSound").play();
}
function change_green() {
    color = "#00ff00";
    document.getElementById("ClickSound").play();
}
function change_blue() {
    color = "#0000ff";
    document.getElementById("ClickSound").play();
}
function change_white() {
    color = "#ffffff";
    document.getElementById("ClickSound").play();
}
function change_8() {
    line = 8;
    document.getElementById("ClickSound").play();
}
function change_16() {
    line = 16;
    document.getElementById("ClickSound").play();
}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+3

最も単純な方法としては, canvas要素の描画状態をgetImageDataメソッドを用いてImageDataオブジェクト化する方法でしょう. このImageDataオブジェクトを保管しておきたい履歴数分, 配列にとっておき, putImageDataメソッドを使ってcanvas要素に書き戻すのです. 後は一般的なundo/redoの実装に帰着します.

簡単なサンプルのみを提示しますので, どのように組み込むかはご自分でお考え下さい.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

//履歴を保管する配列
const history = [];
//履歴の上限数
const limit = 5;

//履歴取得
history.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
if(history.length > limit){
  history.shift();
}

//履歴書き戻し
const id = history.pop();
if(id){
  ctx.putImageData(id, 0, 0);
}


NOTE:
描画状態のオブジェクト化にはImageDataオブジェクトの他にも次のものが利用できます

  • CanvasPatternオブジェクト
  • HTMLCanvasElementオブジェクト(canvas要素)
  • toDataURL(dataURIスキーム形式)/toBlobメソッド

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/01/25 18:06

    ご回答ありがとうございます。ところが本当に申し訳ないことにずぶの素人でいきなり行き詰まってしまいました。
     ご提示の記述を、私のwboard.jsに記載したいのですが、おそらく「履歴取得」をfunction ttouchEndHandler(e) に記載するのだろうと予想します。タッチが離れた瞬間に履歴をとるのですよね?
    書き出しも例えばfunction undoとなり名前を付けて紐づけする感じでしょうか?

    後だしで本当に申し訳ないのですが、VBAくらいしかわからず基本もまったくありません。非常に申し訳ない。。。

    キャンセル

  • 2018/01/25 18:26

    > タッチが離れた瞬間に履歴をとる
    それだとシステムに対する負荷が高すぎます. 1履歴とる毎に多量のメモリ確保・開放が発生するので, 最悪ブラウザがクラッシュします.
    それより例えば一時保存ボタンを用意して, それがクリック・タップされたタイミングで履歴をとるといったふうにしたほうが良かろうと思います.

    キャンセル

  • 2018/01/25 23:45

    なるほど・・・・。
    あきらめきれないのが、
    https://www.kabanoki.net/1823
    様のサイトで紹介されている方法を見ると、さほど負荷があるようにも見えずに・・・。ただこちらはchrome(タブレット)に反応せず流用できませんでした。

    先ほどから様々試してみるのですがうまくいきません・・・。

    キャンセル

  • 2018/01/26 03:32

    負荷の有無はシステムのメモリ容量と画像のサイズ及び画像データの取得頻度で決定します. が, ブラウザによってはこのメモリ負荷に極端に弱いもの(例えばChrome)があります.
    ※もちろんこれは個人的な用途であれば許容できるリスクですが, 不特定多数が利用するWEBページでは無視できない問題です.

    >  ご提示の記述を、私のwboard.jsに記載したいのですが、おそらく「履歴取得」をfunction ttouchEndHandler(e) に記載するのだろうと予想します。タッチが離れた瞬間に履歴をとるのですよね?
    書き出しも例えばfunction undoとなり名前を付けて紐づけする感じでしょうか?
    多分そうなるであろうと(ただ, マルチタッチ絡みの制御が気にはなります).しかし別の質問でありましたとおり, Chromeではローカル環境でのtoDataURL/toBlob/getImageDataは許可されていませんので, エラーとなる場合があります.

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.00%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる