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

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

ただいまの
回答率

89.23%

Canvasで記述した文字に慣性付きの動きを実装したい

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 121

Karna554

score 8

前提・実現したいこと

https://groxi.jp/

先日からこちらのサイトのTOPの文字がマウスカーソルの位置によって移動するところを実装しています。

発生している問題・エラーメッセージ

文字がマウスカーソルの位置に応じて動く部分までは実装できたのですが、そこからその動きに慣性をつけるやり方が見当がつきません。

エラーメッセージ

該当のソースコード

const brown_text_name = ['brown_a_1', 'brown_a_2', 'brown_c', 'brown_H', 'brown_i', 'brown_M', 'brown_p',
'brown_P_1', 'brown_P_2', 'brown_s_1', 'brown_s_2', 'brown_T', 'brown_v'];

// 茶色文字?の設定(X座標、Y座標、角度、フォントの種類&サイズ, 色, 出力文字)
var brown_text = {

    'brown_a_1' : {'pointX': 1144,    'pointY': 333,     'angle': 20,      'font_size': '200px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'a'},
    'brown_a_2' : {'pointX': 823,     'pointY': 427,     'angle': -20,     'font_size': '120px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'a'},
    'brown_c'   : {'pointX': 1097,    'pointY': 605,     'angle': 20,      'font_size': '120px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'c'}, 
    'brown_H'   : {'pointX': -118,    'pointY': 290,     'angle': 340,     'font_size': '330px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'H'},
    'brown_i'   : {'pointX': 1269,    'pointY': 714,     'angle': 20,      'font_size': '230px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'i'},
    'brown_M'   : {'pointX': 430,     'pointY': 12,      'angle': 20,      'font_size': '240px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'M'},
    'brown_p'   : {'pointX': 1321,    'pointY': 656,     'angle': -10,     'font_size': '200px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'p'},
    'brown_P_1' : {'pointX': 669,     'pointY': 0,       'angle': -25,     'font_size': '350px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'P'},
    'brown_P_2' : {'pointX': 1074,    'pointY': 113,     'angle': -25,     'font_size': '230px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'P'},
    'brown_s_1' : {'pointX': 414,     'pointY': 496,     'angle': -30,     'font_size': '270px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 's'},
    'brown_s_2' : {'pointX': 342,     'pointY': 656,     'angle': -20,     'font_size': '120px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 's'},
    'brown_T'   : {'pointX': 209,     'pointY': 178,     'angle': -20,     'font_size': '150px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'T'},
    'brown_v'   : {'pointX': 927,     'pointY': 311,     'angle': -20,     'font_size': '150px Fredoka One', 'color': 'rgba(88, 56, 34, 1)', 'value': 'v'},

};

// 描画用関数
function draw_canvas() {

    // TOP部分の描画コンテキスト取得
    var canvas = document.getElementById('top_canvas');

    // 描画範囲設定
    canvas.width = window.innerWidth;
    canvas.height = 1900;

    // コンテキスト取得
    if (canvas.getContext) {
        var context_brown = canvas.getContext('2d');        // 茶色文字用コンテキスト
    }

    context_brown.textAlign = "left";
    context_brown.textBaseline = "top";

    // 茶色文字描画
    for (let i = 0; i <= brown_text_name.length - 1; i++) {

        context_brown.save();

        var x = brown_text[brown_text_name[i]]['pointX'];
        var y = brown_text[brown_text_name[i]]['pointY'];
        context_brown.font = brown_text[brown_text_name[i]]['font_size'];
        context_brown.fillStyle = brown_text[brown_text_name[i]]['color'];
        context_brown.translate(x, y);
        context_brown.rotate((brown_text[brown_text_name[i]]['angle'] ) * Math.PI / 180);
        context_brown.translate(-x, -y);
        context_brown.fillText(brown_text[brown_text_name[i]]['value'], x, y);

        context_brown.restore();

    }
}

ar preX = 0;
var preY = 0;

// 文字の移動用
window.onload=function(){

    draw_canvas(); // フォント反映

    //マウス移動時のイベントをBODYタグに登録する
    document.body.addEventListener("mousemove", function(e){

      //座標を取得する
      var mX = e.pageX;  //X座標
      var mY = e.pageY;  //Y座標

        var cX = e.pageX;
        var cY = e.pageY;

        var mX = preX - cX;
        var mY = cY - preY;

        preX = cX;
        preY = cY;

        var c = 100;     // 補正変数

        for (let i = 0; i <= brown_text_name.length - 1; i++) {
            brown_text[brown_text_name[i]]['pointX'] += mX / c;
            brown_text[brown_text_name[i]]['pointY'] += mY / c;
        }

        draw_canvas(); // 再描画
    });
}

試したこと

https://qiita.com/edo_m18/items/f0587c3bcd4fb8e2bc50

こちらのサイトを見ながら色々検討してみたのですが、どうにもやり方が見えてきません

  • 気になる質問をクリップする

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

質問への追記・修正、ベストアンサー選択の依頼

  • one_line

    2020/05/21 07:28

    ガン見しています。なんか色々無駄が多いかな。理解するのに時間がかかった。
    ちょっと時間をくださいな。
    ちなみに
    for (let i = 0; i <= brown_text_name.length - 1; i++)
    は、
    for (let i = 0; i < brown_text_name.length; i++)
    にすると、短くなるよ!

    キャンセル

回答 1

checkベストアンサー

+1

見回しているうちに原形をとどめなくなってしまいました。ごめんなさい。
最後の数行を複数定義すれば、同一のページに似たような機能を複数定義できます。
P2 を改造して P3 を作られると 奥行きを醸し出す3D的なものになると思います。
元のプログラムは、文字の座標を直接弄っていましたが、それは好ましくない。
なので、相対的な座標と元座標を加算して描画しています。
それと慣れてきたらグローバル変数を使わないようにしましょう!

<!DOCTYPE html>
<meta charset="utf-8">
<title>Canvas</title>

<body>
<canvas id="top_canvas"></canvas>

<script>
//--------------------------------
//2次の点

class P2 {
  constructor (x = 0, y = 0) { this.x = x; this.y = y; }
  multi ({x, y}) { this.x *= x; this.y *= y; return this; }
  add   ({x, y}) { this.x += x; this.y += y; return this; }
  sub   ({x, y}) { this.x -= x; this.y -= y; return this; }
  copy  () { return new P2 (this.x, this.y); }
}


//--------------------------------
//文字を定義してオブジェクトにする

class Character {
  constructor (value, point, angle, font, fillStyle) {
    this.p = point;
    this.value = value;
    this.angle = angle * Math.PI / 180;
    this.font = font;
    this.fillStyle = fillStyle;
  }

  draw (canvas, offset) {//与えられた canvasオブジェクトを利用して、文字を描く機能を持たせる
    let
      { ctx } = canvas,
      { value, p, angle, font, fillStyle } = this,
      { x, y } = offset.copy ().add (p);

      ctx.textAlign = "left";
      ctx.textBaseline = "top";
      ctx.font = font;
      ctx.fillStyle = fillStyle;
      ctx.save ();
        ctx.translate (x, y);
        ctx.rotate (angle);
        ctx.translate (-x, -y);
        ctx.fillText (value, x, y);
      ctx.restore ();
  }

  static create (value = '', x = 0, y = 0, angle = 0, font = '20px Fredoka One',  fillStyle = 'black') {
    return new Character (value, new P2 (x, y), angle, font, fillStyle);
  }
}


//--------------------------------
// canvas を使いやすいように定義

class Canvas {
  constructor (canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
  }

  setSize (w, h) {
    this.canvas.width = w;
    this.canvas.height = h;
    return this;
  }

  clear () {
    this.canvas.width = this.canvas.width;//これは裏技的
  }

  //オブジェクトを作るときに簡単に定義できるようにするため
  static create (target = document.querySelector ('canvas'), w = 320, h = 200) {
    return (new Canvas (target)).setSize (w, h);
  }
}


//--------------------------------
//マウスの位置を知るためのオブジェクト(マウス移動に連動して関数を呼び出す)

class MousePointer extends P2 {
  constructor (cbFunc, target, x, y) {
    super (x, y);
    this.cbFunc = cbFunc;
    this.target = target;
  }

  //イベントハンドラー
  handleEvent (event) {
    const abs = Math.abs;
    let { pageX: x, pageY: y } = event;
    this.x = x;
    this.y = y;

    //マウスのイベントが発生するたびに登録した関数を呼び出す
    this.cbFunc (event, new P2 (x, y));
  }

  //オブジェクトを作りやすいように、イベントも登録
  static create (cbFunc = null, target = document) {
    if ('function' !== typeof cbFunc)
      throw new Error ('Not function');

    let obj = new MousePointer (cbFunc, target);
    target.addEventListener ('mousemove', obj, false);
    return obj;
  }
}


//--------------------------------
//動きをつかさどるオブジェクト

class Mover  {
  constructor (canvas, elements, area, gain, target_point) {
    this.canvas = canvas;
    this.elements = elements; //array
    this.aP = area;//マウスで動かす移動範囲量
    this.gP = gain;//減速
    this.tP = target_point;//目的地

    this.cP = new P2;//現在地
    this.aniId = null;//アニメーションID
  }

  handleEvent (event, p) {
    this.tP = p.multi (this.aP);//目的地 = マウス座標 * 移動範囲
    if (this.aniId)
      this.aniId = cancelAnimationFrame (this.aniId);
    this.draw ();
  }

  draw () {
    let sa = this.tP.copy ().sub (this.cP).multi (this.gP);//移動距離 = (目的地:複写 - 現在地) * 減速
    let {x, y} = this.cP.add (sa);//現在地 += 移動距離

    this.canvas.clear ();
    this.elements.forEach (t => t.draw (this.canvas, this.cP));//それぞれの文字の描画

    this.aniId = (1 < x || 1 < y)
      ? requestAnimationFrame (this.draw.bind (this))
      : null;
  }

  static create (canvas = Canvas.create (), elements = [], area = new P2 (.5,.5), gain = new P2 (.02, .02), init_position = new P2) {
    return new Mover (canvas, elements, area, gain, init_position);
  }

}


//--------------------------------

//文字をオブジェクト化し配列にする
const TEXT = [
  ['a', 1144, 333,  20, '200px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['a',  823, 427, -20, '120px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['c', 1097, 605,  20, '120px Fredoka One', 'rgba(88, 56, 34, 1)'], 
  ['H', -118, 290, 340, '330px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['i', 1269, 714,  20, '230px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['M',  430,  12,  20, '240px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['p', 1321, 656, -10, '200px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['P',  669,   0, -25, '350px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['P', 1074, 113, -25, '230px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['s',  414, 496, -30, '270px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['s',  342, 656, -20, '120px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['T',  209, 178, -20, '150px Fredoka One', 'rgba(88, 56, 34, 1)'],
  ['v',  927, 311, -20, '150px Fredoka One', 'rgba(88, 56, 34, 1)']
].map (_ => Character.create (..._) );


//キャンバス、ムーバーオブジェクトを作成
const
  canvas = Canvas.create (top_canvas, window.innerWidth, window.innerHeight),
  demo = Mover.create (canvas, TEXT, new P2 (-.3,.3), new P2 (.02,.02));

//マウスポインターのオブジェクトを作成。ついでに動いたときに呼び出す関数を登録
MousePointer.create (demo.handleEvent.bind (demo));
demo.draw ();//最初画面描画

</script>

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2020/05/23 11:13

    わざわざありがとうございます。
    見ずらいコードですみません....

    精進します。

    キャンセル

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

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