20191222のHTMLに関する記事は15件です。

HTML5プロフェッショナル認定試験 Level1を受験して良かったこと

本日、HTML5プロフェッショナル認定試験 Level1に合格しました。
学習過程を振り返って良かったこと・次に活かしたいことをまとめておきます。

試験概要については既にまとめている方がいらっしゃったのでここでは割愛し、振り返りメインでお送りします。
HTML5プロフェッショナル認定資格Level1 合格記

受験の動機

もともとはJavaScriptを体系的に学ぶのに良さそうな試験を探していたときに、この試験を知りました。
ただ、JavaScriptはLevel2にしか出題されず、Level2を受けるにはLevel1に合格する必要があったため、Level1から受験した次第です。

受験してよかったこと

  • より良い実装方法を考えられるようになった
  • お試しの機会が得られた
  • 仕様書を読まねばと気づいた

それぞれ説明します。

より良い実装方法を考えられるようになった

今までHTML,CSSはまとまった仕様書も参考書も読んだことがなかったため、場当たり的な実装しかできていませんでした。
今回HTML、CSSでできることを広く学ぶことで、実装の引き出しがぐっと増えました。
例えば、アルファベットを大文字にしたいとき、今まではJavaScriptで書いていましたが、CSSの text-transform で実装可能とわかりました。(しかもほとんどのブラウザで対応済み)
text-transform - Can I use...
逆に、前から使ってみたかった<datalist> (サジェスト用のリストを作る要素) を試してみたら、スタイルをCSSで上書きできないことが判明し、実利用は難しいと思いました。

このリストのスタイルを変えられない
image.png
入力値が長いと▼が潰れるのもいただけない
image.png

このように、より良い実装方法に気づけたり、知識止まりだった技術の使い所を想像するきっかけが持てたことは、大きな収穫でした。
当初はJavaScriptを勉強したいと思っていましたが、HTML,CSSでできることを先に知っておくことで、より適材適所を意識できるのではと思います。

お試しの機会が得られた

受験にあたって、書いたことのないもの・よく理解できていないものは片っ端からサンプルコードを書いてみました。
例えば、今までメディアクエリはなんとなく「難しそう」と敬遠してフレームワークに任せていましたが、実際にサンプルコードを書くと基本の書式は単純だったので、親しみが持てるようになりました。
サンプルだけでも実装しておくと、いざ業務で応用させる必要が出た時にもハードルが下がるはずです。

仕様書を読まねばと気づいた

サンプルコードを書いていくと、参考書を読んだ時には「わかったつもり」でも、疑問がたくさん出てきます。
例えば、CSSのメディアクエリで device-orientation を使うとスマホの向きによってレイアウトを変えることができますが、それを知った段階では「デバイスを横にしたら横用のスタイル、縦にしたら縦用のスタイルが当たるだろう」と思っていました。しかしそれが間違いでした。PCでサンプルコードを書いたとき、PCは横向きであっても、ブラウザの表示幅を狭くしていくと、「縦用」のスタイルに切り替わったのです。
この事象の詳細はこちらでわかりやすく説明してくださっています。
@mediaのorientationはviewportの縦横がどっちが長いかであって、デバイスが縦か横かじゃないですよ

言葉やイメージに囚われず仕様を確認することが大事だと気づくことができました。

次に活かしたいこと

試験勉強の中での経験から、次に活かしたいことをまとめます。

  • 手を動かしながら覚える
  • 日々仕様書を読む
  • 時々体系的に学ぶ時間をつくる

手を動かしながら覚える

最初は参考書をひたすら読んで問題を説いていく「読書型」の勉強法でした。
ただ、参考書を1,2週したところで自分が過去に線を引いた内容をあまり覚えていないことに気づいたため、「暗記よりも手を動かして理解しよう」と方針を変えました。
そうすると、何度も本で間違えたところが、一度コードを書くことで、すっと理解できるようになりました。
疑問に思ったところは更に検索したり、開発者ツールや拡張機能を使って、自分の実装が「見た目以外も」意図通りになっているかを確認しました。
例えば、どのセクショニングがどうアウトラインに影響するか話は本で何度読んでも忘れたので、サンプルコードを書いて、下記のツールで意図通りの出力になっているかを確認しました。
headingsMap

他にも気になったところは「書く」→「調べる」を繰り返したおかげで理解力が上がりました。この方針転換をしていなければ、合格しても1週間で忘れてしまい、使える知識にならなかったと思います。
参考書の知識を自らの力とする上ではアウトプットは必須と心得ました。

日々仕様書を読む

正直、これまではChromeの開発者ツールやVSCodeが予測してくれる候補の中から「それっぽいもの」をブラウザ上で確認して問題なさそうなら使うことがありました。HTML、CSSはある程度雰囲気で書いても動いてくれるので、仕様を理解して使うのが義務と知りつつ調べずに使ってしまっていたのですが、今回仕様を頻繁に調べる中で、「思ったのと違う」ものが多いことに気づきました。それが今までの行為は他のブラウザでどう見えるかや、他の挙動を起こさないか等の点を考慮できておらず非常に良くなかったと反省し、今後は仕様を調べてから使っていこうと心を改めました。
それに加えて、今まで当たり前に使っていたものも、思い違いや理解していない部分がきっとあるので、時々仕様書をまとめて読み返そうと思います。

時々体系的に学ぶ時間をつくる

今回はHTML,CSSなどWebアプリ開発における基礎知識の大々的な見直しや補完をしたことによって、上記の益を得ることができました。他の技術についても同じことが言えるので、時々このような試験を受けるのはよい気づきの機会だと思いました。

まとめ

この試験に限らずですが、手を動かしながら知識を使って理解すること、それをどう活用できそうかイメージすることが生きた知識につながるのだと思いました。
次は本懐であるLevel2を受験する予定なので、そちらの勉強はじめから手を動かしてやっていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】コピペでできる!フロントエンド3言語でスマホで遊べるテトリス作ってgithubで公開!【PWA】

はじめに

初めまして、@kashiwagi_wataruと申します。

プログラミングを初めて間もない頃って成果物を誰かに使ってもらうことが少なくてモチベーションの意地が結構難しいと思うんですよね。
なんか初心者の人でも誰かに使ってもらえるようなアプリを開発できる記事をかけないかなぁと考えてみました!
そこで思いついたのがコピペでできる。テトリスを作って遊んでもらお!と言う開発から公開まで一連の流れを体験できる記事です。

追記:OUTPUTの鬼となるため、初めてqiitaの記事を書かせていただくことになりました。
乱文、乱コードは寒い日の夜中に子供にプレゼントを届けるサンタのような目でみていただければ幸いです:santa_tone3:

この記事の対象とする人

  • プログラミングを初めてまもない人
  • サーバーサイドが苦手な人
  • 自分が作ったものを誰かに使ってみてもらいたい人
  • 今この記事を見ている人

やりたいことメモ

大人気ゲームテトリスを作る
スマホでも操作可能。
ホーム画面に追加をするとオフラインでも使えようにする
消した行をカウントする。
デプロイとか難しいことはしたくない。AWSで間違ってお金がかかったりするから(うっかり課金経験者ですw)

(AWSでお金がうっかり課金されちゃいガチな人はこちらへ笑 https://qiita.com/Yuji-Ishibashi/items/bb1c0042fd16a9350c5a

目次

大きく分けて3つに分けることができます。

  1. PWAの設定をする
  2. テトリスを作る
  3. githubで公開する

一応なぞってコピペしていくと完成するようになっていますが、
興味あるところまで飛ばしてくださっても全然大丈夫です!

PWAとは?

みなさん、PWAってご存知ですか。
Googleが言い出した、今注目されつつある仕組みで、
これを使えばwebサイトのUIUXをよりよくできるだろうと言われているものになります。

https://www.seohacks.net/basic/terms/pwa/

WAとは、「Progressive Web Apps」の略称で、モバイル向けWebサイトをスマートフォン向けアプリのように使えるようにする仕組みです。
PWAはそれ自体が何か特殊な一つの技術、というわけではありません。レスポンシブデザイン、HTTPS化など、Googleが定める要素を備えたWebサイトであり、オフラインやプッシュ通知に対応するためのブラウザAPI(Service Workerなど)を利用しているWebサイトをPWAと呼びます。
PWAを実装することでプッシュ通知やホーム画面へのアイコン追加など、アプリの特徴的な機能をWebサイトに持たせる事ができます。これにより、UX向上やユーザーエンゲージメントの改善にもつながるとして注目されています。

つまり、webアプリケーションをあたかもスマホアプリのように使えるようになるとのことです!
「ホーム画面に追加」をするとキャッシュさえ残っていればオフラインでもそのwebページが利用可能になるとのこと。。

PWAの導入

PWAの導入はすごく簡単です。

必要なファイルは下記の3つです。

  • index.html
  • service_worker.js
  • manifest.json

圧倒的少なさ!
これだけでゲームが作れるなんて:heart_eyes:

では一つずつ紹介していきますね

index.html

このファイルは一番最初に表示されるページとなります。
スマホでもたくさん使って欲しいのでviewportの記述を忘れずに、
とりあえずは下記のような感じでいいでしょう。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="manifest" href="manifest.json">
    <title>Tetris_app</title>
  </head>
  <body>
  <script>
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('service_worker.js').then(function(registration) {
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function(err) {
          console.log('ServiceWorker registration failed: ', err);
        });
      }
     </script>
   </body>
</html>

service_worker.js

PWAにとってこのファイルはとても大事です。
push通知とかアプリチックなことを実行するときに重要なservice_workerの登録をしてくれます。

service_worker.js
// キャッシュファイルの指定
var CACHE_NAME = 'TetrisApp-caches';
var urlsToCache = [
    '/kashiwagi-wataru.github.io/',
];

// service workerの記述 
    if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service_worker.js').then(function(registration) {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
        console.log('ServiceWorker registration failed: ', err);
    });
    }
// インストール処理
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches
            .open(CACHE_NAME)
            .then(function(cache) {
                return cache.addAll(urlsToCache);
            })
    );
});

// キャッシュロードの処理
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches
            .match(event.request)
            .then(function(response) {
                return response ? response : fetch(event.request);
            })
    );
});

manifest.json

これは、スマホでアプリみたいに利用できるようにするためのファイルです。
下記のような感じでいいでしょう
ポイントは"display": "standalone"です。
standaloneにすることでスマホでアプリのように使用することができます。
もっと拘りたい方は、
https://developer.mozilla.org/ja/docs/Web/Manifest
をみて改造してみてください
アイコンとかを指定できたりもします。

manifest.json
{
    "short_name": "Tetris",
    "name": "Tetris_App",
    "display": "standalone",
    "start_url": "index.html"
}

これで最低限必要なファイルは完成です!(簡単!)

テトリスを作る

今からテトリスを作っていきます。
テトリスといえば知らない人はいないあのゲームのことですよね。
ゲームをつくるといっても使うのは、HTML CSS Javascriptの3つしか使いません。
先ほど作った3つのファイル+cssファイル,jsファイルを作りましょう。
下記のファイルは全て同じ階層に格納されます。

  • index.html(このファイルに上書きしていきます)
  • service_worker.js(これは触らない)
  • manifest.json(これも触らない)

--下記2つを追加--

  • style.css
  • app.js

こんなのを作れればいいなぁと思ってます。

スクリーンショット 2019-12-23 19.04.58.png

テトリスのコードはすごく長くなるのでザーッとみてコピペでいいですw
追々細かい説明を書いていきます笑(2020年になってから笑)

テトリスのコードは下記の通りです。

index.html

先ほど作ったindex.htmlに追記(上書き)していきましょう

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="style.css">
    <link rel="manifest" href="manifest.json">
    <title>PWA Sample</title>
  </head>
  <body>
    <div class="title-wrap">
      <h1>テトリスで遊ぼう!</h1>
    </div>
    <div class="wrapper-container">
      <div class="tetris-container">
        <div class="tetris-main">
          <canvas id="stage" width="250px" height="500px" style="background-color:black;">
          </canvas>
        </div>
        <div class="tetris-option">
          <span class="tetris-panel-container">
            <p>Next</p>
              <canvas id="next" width="150px" height="150px" style="background-color:black;">
              </canvas>
            <p>LINES:<span id="lines">0</span></p>
            <p><span id="message"></span></p>
            <div class="tetris-panel-container-padding">
            <table class="tetris-button-panel">
              <tr>
                <td></td>
                <td id="tetris-rotate-button" class="tetris-button"></td>
                <td></td>
              </tr>
              <tr>
                <td id="tetris-move-left-button"class="tetris-button"></td>
                <td id="tetris-fall-button"class="tetris-button"></td>
                <td id="tetris-move-right-button"class="tetris-button"></td>
              </tr>
            </table>
            </div>
          </span>
        </div>
      </div>
    </div>
    <script src="app.js"></script>
    <script>
      var tetris = new Tetris();
      tetris.startGame();
    </script>
  </body>
</html>

style.css

style.css
html {
    touch-action: manipulation;
}



h1 {
    margin:  0;                 
    position:  relative;        
    font-size: 40px;            
    text-align:  center;        
    margin:  20px 0;            
    display: inline-block;     
}

p {
    margin:  0;                 
    line-height: 2;             
}

 .title1-wrap {
    padding-bottom:  20px;      
    text-align:  center;        
}


.wrapper-container {
    display: inline-block;
}

.tetris-container {
    height:530px;
    display: flex;
    flex-direction: row;
    margin: 10px;
    background-color: #333333;
}

.tetris-panel-contaizner {
    display: flex;
    padding-left: 10px;
    padding-right: 10px;
    flex-direction: column;
    color: white;
    background-color: #333333;
}

.tetris-panel-container-padding {
    flex-grow: 1;
}

.tetris-panel-container p {

   margin-left:20px;
   padding: 10px 0;
   font-family: sans-serif;
   font-size: 20px;
   color: #ffffff;
}

.tetris-button-panel {
    border-style: none;
    width: 100%;
    padding-top:100px;
}

.tetris-button {
    padding-top: 10px;
    padding-bottom: 10px;
    text-align: center;
    background: #444444;
    box-shadow: inset 0 2px 0 rgba(255,255,255,0.2), inset 0 -2px 0 rgba(0, 0, 0, 0.05), 0 2px 6px rgba(0, 0, 0, .15);
    border-radius: 4px;
}

.tetris-button:active {

    box-shadow: 0 0 2px rgba(0, 0, 0, 0.30);
}

app.js

app.js
class Tetris {
    constructor() {
        this.stageWidth = 10;
        this.stageHeight = 20;
        this.stageCanvas = document.getElementById("stage");
        this.nextCanvas = document.getElementById("next");
        let cellWidth = this.stageCanvas.width / this.stageWidth;
        let cellHeight = this.stageCanvas.height / this.stageHeight;
        this.cellSize = cellWidth < cellHeight ? cellWidth : cellHeight;
        this.stageLeftPadding = (this.stageCanvas.width - this.cellSize * this.stageWidth) / 2;
        this.stageTopPadding = (this.stageCanvas.height - this.cellSize * this.stageHeight) ;
        this.blocks = this.createBlocks();
        this.deletedLines = 0;

        window.onkeydown = (e) => {
            if (e.keyCode === 37) {
                this.moveLeft();
            } else if (e.keyCode === 38) {
                this.rotate();
            } else if (e.keyCode === 39) {
                this.moveRight();
            } else if (e.keyCode === 40) {
                this.fall();
            }
        }

        document.getElementById("tetris-move-left-button").onmousedown = (e) => {
            this.moveLeft();
        }
        document.getElementById("tetris-rotate-button").onmousedown = (e) => {
            this.rotate();
        }
        document.getElementById("tetris-move-right-button").onmousedown = (e) => {
            this.moveRight();
        }
        document.getElementById("tetris-fall-button").onmousedown = (e) => {
            this.fall();
        }
    }

    createBlocks() {
        let blocks = [
            {
                shape: [[[-1, 0], [0, 0], [1, 0], [2, 0]],
                        [[0, -1], [0, 0], [0, 1], [0, 2]],
                        [[-1, 0], [0, 0], [1, 0], [2, 0]],
                        [[0, -1], [0, 0], [0, 1], [0, 2]]],
                color: "rgb(0, 255, 255)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 128, 128)"
            },
            {
                shape: [[[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]]],
                color: "rgb(255, 255, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 128, 0)"
            },
            {
                shape: [[[0, 0], [1, 0], [-1, 1], [0, 1]],
                        [[-1, -1], [-1, 0], [0, 0], [0, 1]],
                        [[0, 0], [1, 0], [-1, 1], [0, 1]],
                        [[-1, -1], [-1, 0], [0, 0], [0, 1]]],
                color: "rgb(0, 255, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 128, 0)"
            },
            {
                shape: [[[-1, 0], [0, 0], [0, 1], [1, 1]],
                        [[0, -1], [-1, 0], [0, 0], [-1, 1]],
                        [[-1, 0], [0, 0], [0, 1], [1, 1]],
                        [[0, -1], [-1, 0], [0, 0], [-1, 1]]],
                color: "rgb(255, 0, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 0, 0)"
            },
            {
                shape: [[[-1, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [1, -1], [0, 0], [0, 1]],
                        [[-1, 0], [0, 0], [1, 0], [1, 1]],
                        [[0, -1], [0, 0], [-1, 1], [0, 1]]],
                color: "rgb(0, 0, 255)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 0, 128)"
            },
            {
                shape: [[[1, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [0, 0], [0, 1], [1, 1]],
                        [[-1, 0], [0, 0], [1, 0], [-1, 1]],
                        [[-1, -1], [0, -1], [0, 0], [0, 1]]],
                color: "rgb(255, 165, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 82, 0)"
            },
            {
                shape: [[[0, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [0, 0], [1, 0], [0, 1]],
                        [[-1, 0], [0, 0], [1, 0], [0, 1]],
                        [[0, -1], [-1, 0], [0, 0], [0, 1]]],
                color: "rgb(255, 0, 255)",
                highlight: "rgb(255, 255, 255)",111111111111
                shadow: "rgb(128, 0, 128)"
            }
        ];
        return blocks;
    }

    drawBlock(x, y, type, angle, canvas) {
        let context = canvas.getContext("2d");
        let block = this.blocks[type];
        for (let i = 0; i < block.shape[angle].length; i++) {
            this.drawCell(context,
                     x + (block.shape[angle][i][0] * this.cellSize),
                     y + (block.shape[angle][i][1] * this.cellSize),
                     this.cellSize,
                     type);
        }
    }

    drawCell(context, cellX, cellY, cellSize, type) {
        let block = this.blocks[type];
        let adjustedX = cellX + 0.5;
        let adjustedY = cellY + 0.5;
        let adjustedSize = cellSize - 1;
        context.fillStyle = block.color;
        context.fillRect(adjustedX, adjustedY, adjustedSize, adjustedSize);
        context.strokeStyle = block.highlight;
        context.beginPath();
        context.moveTo(adjustedX, adjustedY + adjustedSize);
        context.lineTo(adjustedX, adjustedY);
        context.lineTo(adjustedX + adjustedSize, adjustedY);
        context.stroke();
        context.strokeStyle = block.shadow;
        context.beginPath();
        context.moveTo(adjustedX, adjustedY + adjustedSize);
        context.lineTo(adjustedX + adjustedSize, adjustedY + adjustedSize);
        context.lineTo(adjustedX + adjustedSize, adjustedY);
        context.stroke();
    }

    startGame() {
        let virtualStage = new Array(this.stageWidth);
        for (let i = 0; i < this.stageWidth; i++) {
            virtualStage[i] = new Array(this.stageHeight).fill(null);
        }
        this.virtualStage = virtualStage;
        this.currentBlock = null;
        this.nextBlock = this.getRandomBlock();
        this.mainLoop();
    }

    mainLoop() {
        if (this.currentBlock == null) {
            if (!this.createNewBlock()) {
                return;
            }
        } else {
            this.fallBlock();
        }
        this.drawStage();
        if (this.currentBlock != null) {
            this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
                this.stageTopPadding + this.blockY * this.cellSize,
                this.currentBlock, this.blockAngle, this.stageCanvas);
        }
        setTimeout(this.mainLoop.bind(this), 500);
    }

    createNewBlock() {
        this.currentBlock = this.nextBlock;
        this.nextBlock = this.getRandomBlock();
        this.blockX = Math.floor(this.stageWidth / 2 - 2);
        this.blockY = 0;
        this.blockAngle = 0;
        this.drawNextBlock();
        if (!this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, this.blockAngle)) {
            let messageElem = document.getElementById("message");
            messageElem.innerText = "GAME OVER";
            return false;
        }
        return true;
    }

    drawNextBlock() {
        this.clear(this.nextCanvas);
        this.drawBlock(this.cellSize * 2, this.cellSize, this.nextBlock,
            0, this.nextCanvas);
    }

    getRandomBlock() {
        return  Math.floor(Math.random() * 7);
    }

    fallBlock() {
        if (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
            this.blockY++;
        } else {
            this.fixBlock(this.blockX, this.blockY, this.currentBlock, this.blockAngle);
            this.currentBlock = null;
        }
    }

    checkBlockMove(x, y, type, angle) {
        for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
            let cellX = x + this.blocks[type].shape[angle][i][0];
            let cellY = y + this.blocks[type].shape[angle][i][1];
            if (cellX < 0 || cellX > this.stageWidth - 1) {
                return false;
            }
            if (cellY > this.stageHeight - 1) {
                return false;
            }
            if (this.virtualStage[cellX][cellY] != null) {
                return false;
            }
        }
        return true;
    }

    fixBlock(x, y, type, angle) {
        for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
            let cellX = x + this.blocks[type].shape[angle][i][0];
            let cellY = y + this.blocks[type].shape[angle][i][1];
            if (cellY >= 0) {
                this.virtualStage[cellX][cellY] = type;
            }
        }
        for (let y = this.stageHeight - 1; y >= 0; ) {
            let filled = true;
            for (let x = 0; x < this.stageWidth; x++) {
                if (this.virtualStage[x][y] == null) {
                    filled = false;
                    break;
                }
            }
            if (filled) {
                for (let y2 = y; y2 > 0; y2--) {
                    for (let x = 0; x < this.stageWidth; x++) {
                        this.virtualStage[x][y2] = this.virtualStage[x][y2 - 1];
                    }
                }
                for (let x = 0; x < this.stageWidth; x++) {
                    this.virtualStage[x][0] = null;
                }
            let linesElem = document.getElementById("lines");
                this.deletedLines++;
                linesElem.innerText = "" + this.deletedLines;
            } else {
                y--;
            }
        }
    }

    drawStage() {
        this.clear(this.stageCanvas);

        let context = this.stageCanvas.getContext("2d");
        for (let x = 0; x < this.virtualStage.length; x++) {
            for (let y = 0; y < this.virtualStage[x].length; y++) {
                if (this.virtualStage[x][y] != null) {
                    this.drawCell(context,
                        this.stageLeftPadding + (x * this.cellSize),
                        this.stageTopPadding + (y * this.cellSize),
                        this.cellSize,
                        this.virtualStage[x][y]);
                }
            }
        }
    }

    moveLeft() {
        if (this.checkBlockMove(this.blockX - 1, this.blockY, this.currentBlock, this.blockAngle)) {
            this.blockX--;
            this.refreshStage();
        }
    }

    moveRight() {
        if (this.checkBlockMove(this.blockX + 1, this.blockY, this.currentBlock, this.blockAngle)) {
            this.blockX++;
            this.refreshStage();
        }
    }

    rotate() {
        let newAngle;
        if (this.blockAngle < 3) {
            newAngle = this.blockAngle + 1;
        } else {
            newAngle = 0;
        }
        if (this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, newAngle)) {
            this.blockAngle = newAngle;
            this.refreshStage();
        }
    }

    fall() {
        while (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
            this.blockY++;
            this.refreshStage();
        }
    }

    refreshStage() {
      this.clear(this.stageCanvas);
      this.drawStage();
      this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
                this.stageTopPadding + this.blockY * this.cellSize,
                this.currentBlock, this.blockAngle, this.stageCanvas);
    }

    clear(canvas) {
        let context = canvas.getContext("2d");
        context.fillStyle = "rgb(0, 0, 0)";
        context.fillRect(0, 0, canvas.width, canvas.height);
    }
}

githubで公開する!!

では作成したファイルをサーバーに公開して終了となります。
サーバーはhttpsが利用できるAWSやherokuなどで頑張って公開しようかと思っていたところ、
githubだけで簡単にwebページを公開する方法があるそうです。

しかもgithubの公式さんが言うには、かなり簡単とのこと。
これはやるしかないですね:raised_hands_tone3:

github公式が割とわかりやすいのでおいておきますね。
https://pages.github.com/

github.ioと言うものらしいです。

github.ioのメリットデメリット

メリット

  • 簡単にwebページを公開できる
  • 無料
  • 更新したい場合はリポジトリにpushするだけ

デメリット

  • ソースコードが公開される
  • サーバー依存のjsとかは使えない

今回は...

  • ただ自分で作ったwebページを公開するだけ(特にセキリュティを気にするものはない)
  • jsはサーバーに依存しない

と言うことで最適な方法と言えるのではないのでしょうか。

方法

やり方は簡単で、
リポジトリ名を
username.github.io
にするだけでいいらしいです。すごい。

ではやってみましょう。(本当にほぼ公式のやり方をなぞっていくだけですw)

① まずはリポジトリを作ります。
命名規則はusername.github.io だそうです。
僕の場合は下記のようになります。

スクリーンショット 2019-12-23 13.24.24.png

② 次に下記のコマンドを実行します。デスクトップとかでね

クローンして

git clone https://github.com/username/username.github.io

移動してサンプルテキストを入れて(index.htmlを作って)

cd username.github.io

echo "Hello World" > index.html
git add --all

git commit -m "Initial commit"

git push -u origin master

そして https://username.github.io にアクセスすればHelloWorldと表示されるはず!

スクリーンショット 2019-12-23 13.48.09.png

:astonished: :astonished: :astonished: :astonished:
なぜか404と怒られてしまいました。。。

原因はよくわかりませんw
ですが作ったリポジトリのsettings にある
スクリーンショット 2019-12-23 13.50.54.png
ここにリンクが載っていました。
リンクの構造がgithub公式の構造と少し違いますが、とりあえず踏んでみましょう。
スクリーンショット 2019-12-23 13.52.55.png

表示されました!
(反映されるまで、少し時間が(30秒くらい)かかるかもしれないので焦らずに待ちましょう)

これで作ったゲームを公開する準備はできました。

このリポジトリにテトリスのファイルをぶち込んで完成となります。

動作確認

最終的なファイルは
- index.html
- style.css
- app.js
- manifest.json
- service_worker.js

の5つになると思います。

githubのリポジトリに全てのファイルが揃ったらhttps://username.github.ioにアクセスしてみましょう。
PC、スマホ両方でアクセスすると良いと思います。

SmartSelect_20191223-185402_One UI Home.jpg

こんな感じでChromeとは別のタブで開けていたらOKです!
オフラインでの動作を確認したいので一度タブを閉じで機内モードにしてから開いてみてください:information_desk_person:
機内モードでもテトリスで遊べちゃうと思います!
恐るべしPWA。。。

こんな感じで、テトリスをスマホアプリのように扱えるようになりました。
リンクを友達に教えると友達のスマホでも遊べるようになるのでぜひ友達に送りつけてみてください。

参照記事

結構コピペしてますw:bow_tone2:
https://paiza.hatenablog.com/entry/2018/08/29/PWA%E5%85%A5%E9%96%80%EF%BC%81JavaScript%E3%81%A7%E7%B0%A1%E5%8D%98%E3%81%AB%E9%AB%98%E9%80%9F%E5%8C%96%EF%BC%86%E3%82%AA%E3%83%95%E3%83%A9%E3%82%A4%E3%83%B3%E5%AF%BE%E5%BF%9C%E3%81%AEWeb%E3%82%B5

http://kmaebashi.com/programmer/tetris/index.html

https://www.webprofessional.jp/what-is-the-attention-project-pwa-by-google/

https://www.seohacks.net/basic/terms/pwa/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】【PWA】コピペでできる!フロントエンド3言語でテトリス作ってスマホで遊ぼ!【githubで公開】

はじめに

初めまして、@kashiwagi_wataruと申します。

プログラミングを初めて間もない頃って成果物を誰かに使ってもらうことが少なくてモチベーションの意地が結構難しいと思うんですよね。
なんか初心者の人でも誰かに使ってもらえるようなアプリを開発できる記事をかけないかなぁと考えてみました!
そこで思いついたのがコピペでできる。テトリスを作って遊んでもらお!と言う開発から公開まで一連の流れを体験できる記事です。

追記:OUTPUTの鬼となるため、初めてqiitaの記事を書かせていただくことになりました。
乱文、乱コードは寒い日の夜中に子供にプレゼントを届けるサンタのような目でみていただければ幸いです:santa_tone3:

この記事の対象とする人

  • プログラミングを初めてまもない人
  • サーバーサイドが苦手な人
  • 自分が作ったものを誰かに使ってみてもらいたい人
  • 今この記事を見ている人

やりたいことメモ

大人気ゲームテトリスを作る
スマホでも操作可能。
ホーム画面に追加をするとオフラインでも使えようにする
消した行をカウントする。
デプロイとか難しいことはしたくない。AWSで間違ってお金がかかったりするから(うっかり課金経験者ですw)

(AWSでお金がうっかり課金されちゃいガチな人はこちらへ笑 https://qiita.com/Yuji-Ishibashi/items/bb1c0042fd16a9350c5a

目次

大きく分けて3つに分けることができます。

  1. PWAの設定をする
  2. テトリスを作る
  3. githubで公開する

一応なぞってコピペしていくと完成するようになっていますが、
興味あるところまで飛ばしてくださっても全然大丈夫です!

PWAとは?

みなさん、PWAってご存知ですか。
Googleが言い出した、今注目されつつある仕組みで、
これを使えばwebサイトのUIUXをよりよくできるだろうと言われているものになります。

https://www.seohacks.net/basic/terms/pwa/

WAとは、「Progressive Web Apps」の略称で、モバイル向けWebサイトをスマートフォン向けアプリのように使えるようにする仕組みです。
PWAはそれ自体が何か特殊な一つの技術、というわけではありません。レスポンシブデザイン、HTTPS化など、Googleが定める要素を備えたWebサイトであり、オフラインやプッシュ通知に対応するためのブラウザAPI(Service Workerなど)を利用しているWebサイトをPWAと呼びます。
PWAを実装することでプッシュ通知やホーム画面へのアイコン追加など、アプリの特徴的な機能をWebサイトに持たせる事ができます。これにより、UX向上やユーザーエンゲージメントの改善にもつながるとして注目されています。

つまり、webアプリケーションをあたかもスマホアプリのように使えるようになるとのことです!
「ホーム画面に追加」をするとキャッシュさえ残っていればオフラインでもそのwebページが利用可能になるとのこと。。

PWAの導入

PWAの導入はすごく簡単です。

必要なファイルは下記の3つです。

  • index.html
  • service_worker.js
  • manifest.json

圧倒的少なさ!
これだけでゲームが作れるなんて:heart_eyes:

では一つずつ紹介していきますね

index.html

このファイルは一番最初に表示されるページとなります。
スマホでもたくさん使って欲しいのでviewportの記述を忘れずに、
とりあえずは下記のような感じでいいでしょう。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="manifest" href="manifest.json">
    <title>Tetris_app</title>
  </head>
  <body>
  <script>
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('service_worker.js').then(function(registration) {
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function(err) {
          console.log('ServiceWorker registration failed: ', err);
        });
      }
     </script>
   </body>
</html>

service_worker.js

PWAにとってこのファイルはとても大事です。
push通知とかアプリチックなことを実行するときに重要なservice_workerの登録をしてくれます。

service_worker.js
// キャッシュファイルの指定
var CACHE_NAME = 'TetrisApp-caches';
var urlsToCache = [
    '/kashiwagi-wataru.github.io/',
];

// service workerの記述 
    if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service_worker.js').then(function(registration) {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
        console.log('ServiceWorker registration failed: ', err);
    });
    }
// インストール処理
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches
            .open(CACHE_NAME)
            .then(function(cache) {
                return cache.addAll(urlsToCache);
            })
    );
});

// キャッシュロードの処理
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches
            .match(event.request)
            .then(function(response) {
                return response ? response : fetch(event.request);
            })
    );
});

manifest.json

これは、スマホでアプリみたいに利用できるようにするためのファイルです。
下記のような感じでいいでしょう
ポイントは"display": "standalone"です。
standaloneにすることでスマホでアプリのように使用することができます。
もっと拘りたい方は、
https://developer.mozilla.org/ja/docs/Web/Manifest
をみて改造してみてください
アイコンとかを指定できたりもします。

manifest.json
{
    "short_name": "Tetris",
    "name": "Tetris_App",
    "display": "standalone",
    "start_url": "index.html"
}

これで最低限必要なファイルは完成です!(簡単!)

テトリスを作る

今からテトリスを作っていきます。
テトリスといえば知らない人はいないあのゲームのことですよね。
ゲームをつくるといっても使うのは、HTML CSS Javascriptの3つしか使いません。
先ほど作った3つのファイル+cssファイル,jsファイルを作りましょう。
下記のファイルは全て同じ階層に格納されます。

  • index.html(このファイルに上書きしていきます)
  • service_worker.js(これは触らない)
  • manifest.json(これも触らない)

--下記2つを追加--

  • style.css
  • app.js

こんなのを作れればいいなぁと思ってます。

スクリーンショット 2019-12-23 19.04.58.png

テトリスのコードはすごく長くなるのでザーッとみてコピペでいいですw
追々細かい説明を書いていきます笑(2020年になってから笑)

テトリスのコードは下記の通りです。

index.html

先ほど作ったindex.htmlに追記(上書き)していきましょう

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="style.css">
    <link rel="manifest" href="manifest.json">
    <title>PWA Sample</title>
  </head>
  <body>
    <div class="title-wrap">
      <h1>テトリスで遊ぼう!</h1>
    </div>
    <div class="wrapper-container">
      <div class="tetris-container">
        <div class="tetris-main">
          <canvas id="stage" width="250px" height="500px" style="background-color:black;">
          </canvas>
        </div>
        <div class="tetris-option">
          <span class="tetris-panel-container">
            <p>Next</p>
              <canvas id="next" width="150px" height="150px" style="background-color:black;">
              </canvas>
            <p>LINES:<span id="lines">0</span></p>
            <p><span id="message"></span></p>
            <div class="tetris-panel-container-padding">
            <table class="tetris-button-panel">
              <tr>
                <td></td>
                <td id="tetris-rotate-button" class="tetris-button"></td>
                <td></td>
              </tr>
              <tr>
                <td id="tetris-move-left-button"class="tetris-button"></td>
                <td id="tetris-fall-button"class="tetris-button"></td>
                <td id="tetris-move-right-button"class="tetris-button"></td>
              </tr>
            </table>
            </div>
          </span>
        </div>
      </div>
    </div>
    <script src="app.js"></script>
    <script>
      var tetris = new Tetris();
      tetris.startGame();
    </script>
  </body>
</html>

style.css

style.css
html {
    touch-action: manipulation;
}



h1 {
    margin:  0;                 
    position:  relative;        
    font-size: 40px;            
    text-align:  center;        
    margin:  20px 0;            
    display: inline-block;     
}

p {
    margin:  0;                 
    line-height: 2;             
}

 .title1-wrap {
    padding-bottom:  20px;      
    text-align:  center;        
}


.wrapper-container {
    display: inline-block;
}

.tetris-container {
    height:530px;
    display: flex;
    flex-direction: row;
    margin: 10px;
    background-color: #333333;
}

.tetris-panel-contaizner {
    display: flex;
    padding-left: 10px;
    padding-right: 10px;
    flex-direction: column;
    color: white;
    background-color: #333333;
}

.tetris-panel-container-padding {
    flex-grow: 1;
}

.tetris-panel-container p {

   margin-left:20px;
   padding: 10px 0;
   font-family: sans-serif;
   font-size: 20px;
   color: #ffffff;
}

.tetris-button-panel {
    border-style: none;
    width: 100%;
    padding-top:100px;
}

.tetris-button {
    padding-top: 10px;
    padding-bottom: 10px;
    text-align: center;
    background: #444444;
    box-shadow: inset 0 2px 0 rgba(255,255,255,0.2), inset 0 -2px 0 rgba(0, 0, 0, 0.05), 0 2px 6px rgba(0, 0, 0, .15);
    border-radius: 4px;
}

.tetris-button:active {

    box-shadow: 0 0 2px rgba(0, 0, 0, 0.30);
}

app.js

app.js
class Tetris {
    constructor() {
        this.stageWidth = 10;
        this.stageHeight = 20;
        this.stageCanvas = document.getElementById("stage");
        this.nextCanvas = document.getElementById("next");
        let cellWidth = this.stageCanvas.width / this.stageWidth;
        let cellHeight = this.stageCanvas.height / this.stageHeight;
        this.cellSize = cellWidth < cellHeight ? cellWidth : cellHeight;
        this.stageLeftPadding = (this.stageCanvas.width - this.cellSize * this.stageWidth) / 2;
        this.stageTopPadding = (this.stageCanvas.height - this.cellSize * this.stageHeight) ;
        this.blocks = this.createBlocks();
        this.deletedLines = 0;

        window.onkeydown = (e) => {
            if (e.keyCode === 37) {
                this.moveLeft();
            } else if (e.keyCode === 38) {
                this.rotate();
            } else if (e.keyCode === 39) {
                this.moveRight();
            } else if (e.keyCode === 40) {
                this.fall();
            }
        }

        document.getElementById("tetris-move-left-button").onmousedown = (e) => {
            this.moveLeft();
        }
        document.getElementById("tetris-rotate-button").onmousedown = (e) => {
            this.rotate();
        }
        document.getElementById("tetris-move-right-button").onmousedown = (e) => {
            this.moveRight();
        }
        document.getElementById("tetris-fall-button").onmousedown = (e) => {
            this.fall();
        }
    }

    createBlocks() {
        let blocks = [
            {
                shape: [[[-1, 0], [0, 0], [1, 0], [2, 0]],
                        [[0, -1], [0, 0], [0, 1], [0, 2]],
                        [[-1, 0], [0, 0], [1, 0], [2, 0]],
                        [[0, -1], [0, 0], [0, 1], [0, 2]]],
                color: "rgb(0, 255, 255)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 128, 128)"
            },
            {
                shape: [[[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]]],
                color: "rgb(255, 255, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 128, 0)"
            },
            {
                shape: [[[0, 0], [1, 0], [-1, 1], [0, 1]],
                        [[-1, -1], [-1, 0], [0, 0], [0, 1]],
                        [[0, 0], [1, 0], [-1, 1], [0, 1]],
                        [[-1, -1], [-1, 0], [0, 0], [0, 1]]],
                color: "rgb(0, 255, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 128, 0)"
            },
            {
                shape: [[[-1, 0], [0, 0], [0, 1], [1, 1]],
                        [[0, -1], [-1, 0], [0, 0], [-1, 1]],
                        [[-1, 0], [0, 0], [0, 1], [1, 1]],
                        [[0, -1], [-1, 0], [0, 0], [-1, 1]]],
                color: "rgb(255, 0, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 0, 0)"
            },
            {
                shape: [[[-1, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [1, -1], [0, 0], [0, 1]],
                        [[-1, 0], [0, 0], [1, 0], [1, 1]],
                        [[0, -1], [0, 0], [-1, 1], [0, 1]]],
                color: "rgb(0, 0, 255)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 0, 128)"
            },
            {
                shape: [[[1, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [0, 0], [0, 1], [1, 1]],
                        [[-1, 0], [0, 0], [1, 0], [-1, 1]],
                        [[-1, -1], [0, -1], [0, 0], [0, 1]]],
                color: "rgb(255, 165, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 82, 0)"
            },
            {
                shape: [[[0, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [0, 0], [1, 0], [0, 1]],
                        [[-1, 0], [0, 0], [1, 0], [0, 1]],
                        [[0, -1], [-1, 0], [0, 0], [0, 1]]],
                color: "rgb(255, 0, 255)",
                highlight: "rgb(255, 255, 255)",111111111111
                shadow: "rgb(128, 0, 128)"
            }
        ];
        return blocks;
    }

    drawBlock(x, y, type, angle, canvas) {
        let context = canvas.getContext("2d");
        let block = this.blocks[type];
        for (let i = 0; i < block.shape[angle].length; i++) {
            this.drawCell(context,
                     x + (block.shape[angle][i][0] * this.cellSize),
                     y + (block.shape[angle][i][1] * this.cellSize),
                     this.cellSize,
                     type);
        }
    }

    drawCell(context, cellX, cellY, cellSize, type) {
        let block = this.blocks[type];
        let adjustedX = cellX + 0.5;
        let adjustedY = cellY + 0.5;
        let adjustedSize = cellSize - 1;
        context.fillStyle = block.color;
        context.fillRect(adjustedX, adjustedY, adjustedSize, adjustedSize);
        context.strokeStyle = block.highlight;
        context.beginPath();
        context.moveTo(adjustedX, adjustedY + adjustedSize);
        context.lineTo(adjustedX, adjustedY);
        context.lineTo(adjustedX + adjustedSize, adjustedY);
        context.stroke();
        context.strokeStyle = block.shadow;
        context.beginPath();
        context.moveTo(adjustedX, adjustedY + adjustedSize);
        context.lineTo(adjustedX + adjustedSize, adjustedY + adjustedSize);
        context.lineTo(adjustedX + adjustedSize, adjustedY);
        context.stroke();
    }

    startGame() {
        let virtualStage = new Array(this.stageWidth);
        for (let i = 0; i < this.stageWidth; i++) {
            virtualStage[i] = new Array(this.stageHeight).fill(null);
        }
        this.virtualStage = virtualStage;
        this.currentBlock = null;
        this.nextBlock = this.getRandomBlock();
        this.mainLoop();
    }

    mainLoop() {
        if (this.currentBlock == null) {
            if (!this.createNewBlock()) {
                return;
            }
        } else {
            this.fallBlock();
        }
        this.drawStage();
        if (this.currentBlock != null) {
            this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
                this.stageTopPadding + this.blockY * this.cellSize,
                this.currentBlock, this.blockAngle, this.stageCanvas);
        }
        setTimeout(this.mainLoop.bind(this), 500);
    }

    createNewBlock() {
        this.currentBlock = this.nextBlock;
        this.nextBlock = this.getRandomBlock();
        this.blockX = Math.floor(this.stageWidth / 2 - 2);
        this.blockY = 0;
        this.blockAngle = 0;
        this.drawNextBlock();
        if (!this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, this.blockAngle)) {
            let messageElem = document.getElementById("message");
            messageElem.innerText = "GAME OVER";
            return false;
        }
        return true;
    }

    drawNextBlock() {
        this.clear(this.nextCanvas);
        this.drawBlock(this.cellSize * 2, this.cellSize, this.nextBlock,
            0, this.nextCanvas);
    }

    getRandomBlock() {
        return  Math.floor(Math.random() * 7);
    }

    fallBlock() {
        if (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
            this.blockY++;
        } else {
            this.fixBlock(this.blockX, this.blockY, this.currentBlock, this.blockAngle);
            this.currentBlock = null;
        }
    }

    checkBlockMove(x, y, type, angle) {
        for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
            let cellX = x + this.blocks[type].shape[angle][i][0];
            let cellY = y + this.blocks[type].shape[angle][i][1];
            if (cellX < 0 || cellX > this.stageWidth - 1) {
                return false;
            }
            if (cellY > this.stageHeight - 1) {
                return false;
            }
            if (this.virtualStage[cellX][cellY] != null) {
                return false;
            }
        }
        return true;
    }

    fixBlock(x, y, type, angle) {
        for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
            let cellX = x + this.blocks[type].shape[angle][i][0];
            let cellY = y + this.blocks[type].shape[angle][i][1];
            if (cellY >= 0) {
                this.virtualStage[cellX][cellY] = type;
            }
        }
        for (let y = this.stageHeight - 1; y >= 0; ) {
            let filled = true;
            for (let x = 0; x < this.stageWidth; x++) {
                if (this.virtualStage[x][y] == null) {
                    filled = false;
                    break;
                }
            }
            if (filled) {
                for (let y2 = y; y2 > 0; y2--) {
                    for (let x = 0; x < this.stageWidth; x++) {
                        this.virtualStage[x][y2] = this.virtualStage[x][y2 - 1];
                    }
                }
                for (let x = 0; x < this.stageWidth; x++) {
                    this.virtualStage[x][0] = null;
                }
            let linesElem = document.getElementById("lines");
                this.deletedLines++;
                linesElem.innerText = "" + this.deletedLines;
            } else {
                y--;
            }
        }
    }

    drawStage() {
        this.clear(this.stageCanvas);

        let context = this.stageCanvas.getContext("2d");
        for (let x = 0; x < this.virtualStage.length; x++) {
            for (let y = 0; y < this.virtualStage[x].length; y++) {
                if (this.virtualStage[x][y] != null) {
                    this.drawCell(context,
                        this.stageLeftPadding + (x * this.cellSize),
                        this.stageTopPadding + (y * this.cellSize),
                        this.cellSize,
                        this.virtualStage[x][y]);
                }
            }
        }
    }

    moveLeft() {
        if (this.checkBlockMove(this.blockX - 1, this.blockY, this.currentBlock, this.blockAngle)) {
            this.blockX--;
            this.refreshStage();
        }
    }

    moveRight() {
        if (this.checkBlockMove(this.blockX + 1, this.blockY, this.currentBlock, this.blockAngle)) {
            this.blockX++;
            this.refreshStage();
        }
    }

    rotate() {
        let newAngle;
        if (this.blockAngle < 3) {
            newAngle = this.blockAngle + 1;
        } else {
            newAngle = 0;
        }
        if (this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, newAngle)) {
            this.blockAngle = newAngle;
            this.refreshStage();
        }
    }

    fall() {
        while (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
            this.blockY++;
            this.refreshStage();
        }
    }

    refreshStage() {
      this.clear(this.stageCanvas);
      this.drawStage();
      this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
                this.stageTopPadding + this.blockY * this.cellSize,
                this.currentBlock, this.blockAngle, this.stageCanvas);
    }

    clear(canvas) {
        let context = canvas.getContext("2d");
        context.fillStyle = "rgb(0, 0, 0)";
        context.fillRect(0, 0, canvas.width, canvas.height);
    }
}

githubで公開する!!

では作成したファイルをサーバーに公開して終了となります。
サーバーはhttpsが利用できるAWSやherokuなどで頑張って公開しようかと思っていたところ、
githubだけで簡単にwebページを公開する方法があるそうです。

しかもgithubの公式さんが言うには、かなり簡単とのこと。
これはやるしかないですね:raised_hands_tone3:

github公式が割とわかりやすいのでおいておきますね。
https://pages.github.com/

github.ioと言うものらしいです。

github.ioのメリットデメリット

メリット

  • 簡単にwebページを公開できる
  • 無料
  • 更新したい場合はリポジトリにpushするだけ

デメリット

  • ソースコードが公開される
  • サーバー依存のjsとかは使えない

今回は...

  • ただ自分で作ったwebページを公開するだけ(特にセキリュティを気にするものはない)
  • jsはサーバーに依存しない

と言うことで最適な方法と言えるのではないのでしょうか。

方法

やり方は簡単で、
リポジトリ名を
username.github.io
にするだけでいいらしいです。すごい。

ではやってみましょう。(本当にほぼ公式のやり方をなぞっていくだけですw)

① まずはリポジトリを作ります。
命名規則はusername.github.io だそうです。
僕の場合は下記のようになります。

スクリーンショット 2019-12-23 13.24.24.png

② 次に下記のコマンドを実行します。デスクトップとかでね

クローンして

git clone https://github.com/username/username.github.io

移動してサンプルテキストを入れて(index.htmlを作って)

cd username.github.io

echo "Hello World" > index.html
git add --all

git commit -m "Initial commit"

git push -u origin master

そして https://username.github.io にアクセスすればHelloWorldと表示されるはず!

スクリーンショット 2019-12-23 13.48.09.png

:astonished: :astonished: :astonished: :astonished:
なぜか404と怒られてしまいました。。。

原因はよくわかりませんw
ですが作ったリポジトリのsettings にある
スクリーンショット 2019-12-23 13.50.54.png
ここにリンクが載っていました。
リンクの構造がgithub公式の構造と少し違いますが、とりあえず踏んでみましょう。
スクリーンショット 2019-12-23 13.52.55.png

表示されました!
(反映されるまで、少し時間が(30秒くらい)かかるかもしれないので焦らずに待ちましょう)

これで作ったゲームを公開する準備はできました。

このリポジトリにテトリスのファイルをぶち込んで完成となります。

動作確認

最終的なファイルは
- index.html
- style.css
- app.js
- manifest.json
- service_worker.js

の5つになると思います。

githubのリポジトリに全てのファイルが揃ったらhttps://username.github.ioにアクセスしてみましょう。
PC、スマホ両方でアクセスすると良いと思います。

SmartSelect_20191223-185402_One UI Home.jpg

こんな感じでChromeとは別のタブで開けていたらOKです!
オフラインでの動作を確認したいので一度タブを閉じで機内モードにしてから開いてみてください:information_desk_person:
機内モードでもテトリスで遊べちゃうと思います!
恐るべしPWA。。。

こんな感じで、テトリスをスマホアプリのように扱えるようになりました。
リンクを友達に教えると友達のスマホでも遊べるようになるのでぜひ友達に送りつけてみてください。

参照記事

結構コピペしてますw:bow_tone2:
https://paiza.hatenablog.com/entry/2018/08/29/PWA%E5%85%A5%E9%96%80%EF%BC%81JavaScript%E3%81%A7%E7%B0%A1%E5%8D%98%E3%81%AB%E9%AB%98%E9%80%9F%E5%8C%96%EF%BC%86%E3%82%AA%E3%83%95%E3%83%A9%E3%82%A4%E3%83%B3%E5%AF%BE%E5%BF%9C%E3%81%AEWeb%E3%82%B5

http://kmaebashi.com/programmer/tetris/index.html

https://www.webprofessional.jp/what-is-the-attention-project-pwa-by-google/

https://www.seohacks.net/basic/terms/pwa/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】コピペでできる!javascriptでスマホで遊べるテトリス作ってgithubで公開!【PWA】

はじめに

初めまして、@kashiwagi_wataruと申します。

プログラミングを初めて間もない頃って成果物を誰かに使ってもらうことが少なくてモチベーションの意地が結構難しいと思うんですよね。
なんか初心者の人でも誰かに使ってもらえるようなアプリを開発できる記事をかけないかなぁと考えてみました!
そこで思いついたのがコピペでできる。テトリスを作って遊んでもらお!と言う開発から公開まで一連の流れを体験できる記事です。

追記:OUTPUTの鬼となるため、初めてqiitaの記事を書かせていただくことになりました。
乱文、乱コードは寒い日の夜中に子供にプレゼントを届けるサンタのような目でみていただければ幸いです:santa_tone3:

この記事の対象とする人

  • プログラミングを初めてまもない人
  • サーバーサイドが苦手な人
  • 自分が作ったものを誰かに使ってみてもらいたい人
  • 今この記事を見ている人

やりたいことメモ

大人気ゲームテトリスを作る
スマホでも操作可能。
ホーム画面に追加をするとオフラインでも使えようにする
消した行をカウントする。
デプロイとか難しいことはしたくない。AWSで間違ってお金がかかったりするから(経験者ですw)

(AWSでお金がうっかり課金されちゃいガチな人はこちらへ笑 https://qiita.com/Yuji-Ishibashi/items/bb1c0042fd16a9350c5a

目次

大きく分けて3つに分けることができます。

  1. PWAの設定をする
  2. テトリスを作る
  3. githubで公開する

一応なぞってコピペしていくと完成するようになっていますが、
興味あるところまで飛ばしてくださっても全然大丈夫です!

PWAとは?

みなさん、PWAってご存知ですか。
Googleが言い出した、今注目されつつある仕組みで、
これを使えばwebサイトのUIUXをよりよくできるだろうと言われているものになります。

https://www.seohacks.net/basic/terms/pwa/

WAとは、「Progressive Web Apps」の略称で、モバイル向けWebサイトをスマートフォン向けアプリのように使えるようにする仕組みです。
PWAはそれ自体が何か特殊な一つの技術、というわけではありません。レスポンシブデザイン、HTTPS化など、Googleが定める要素を備えたWebサイトであり、オフラインやプッシュ通知に対応するためのブラウザAPI(Service Workerなど)を利用しているWebサイトをPWAと呼びます。
PWAを実装することでプッシュ通知やホーム画面へのアイコン追加など、アプリの特徴的な機能をWebサイトに持たせる事ができます。これにより、UX向上やユーザーエンゲージメントの改善にもつながるとして注目されています。

つまり、webアプリケーションをあたかもスマホアプリのように使えるようになるとのことです!
「ホーム画面に追加」をするとキャッシュさえ残っていればオフラインでもそのwebページが利用可能になるとのこと。。

PWAの導入

PWAの導入はすごく簡単です。

必要なファイルは下記の3つです。

  • index.html
  • service_worker.js
  • manifest.json

圧倒的少なさ!
これだけでゲームが作れるなんて:heart_eyes:

では一つずつ紹介していきますね

index.html

このファイルは一番最初に表示されるページとなります。
スマホでもたくさん使って欲しいのでviewportの記述を忘れずに、
とりあえずは下記のような感じでいいでしょう。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="manifest" href="manifest.json">
    <title>Tetris_app</title>
  </head>
  <body>
  <script>
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('service_worker.js').then(function(registration) {
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function(err) {
          console.log('ServiceWorker registration failed: ', err);
        });
      }
     </script>
   </body>
</html>

service_worker.js

PWAにとってこのファイルはとても大事です。
push通知とかアプリチックなことを実行するときに重要なservice_workerの登録をしてくれます。

service_worker.js
// キャッシュファイルの指定
var CACHE_NAME = 'TetrisApp-caches';
var urlsToCache = [
    '/kashiwagi-wataru.github.io/',
];

// service workerの記述 
    if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service_worker.js').then(function(registration) {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
        console.log('ServiceWorker registration failed: ', err);
    });
    }
// インストール処理
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches
            .open(CACHE_NAME)
            .then(function(cache) {
                return cache.addAll(urlsToCache);
            })
    );
});

// キャッシュロードの処理
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches
            .match(event.request)
            .then(function(response) {
                return response ? response : fetch(event.request);
            })
    );
});

manifest.json

これは、スマホでアプリみたいに利用できるようにするためのファイルです。
下記のような感じでいいでしょう
ポイントは"display": "standalone"です。
standaloneにすることでスマホでアプリのように使用することができます。
もっと拘りたい方は、
https://developer.mozilla.org/ja/docs/Web/Manifest
をみて改造してみてください
アイコンとかを指定できたりもします。

manifest.json
{
    "short_name": "Tetris",
    "name": "Tetris_App",
    "display": "standalone",
    "start_url": "index.html"
}

これで最低限必要なファイルは完成です!(簡単!)

テトリスを作る

今からテトリスを作っていきます。
テトリスといえば知らない人はいないあのゲームのことですよね。
ゲームをつくるといっても使うのは、HTML CSS Javascriptの3つしか使いません。
先ほど作った3つのファイル+cssファイル,jsファイルを作りましょう。
下記のファイルは全て同じ階層に格納されます。

  • index.html(このファイルに上書きしていきます)
  • service_worker.js(これは触らない)
  • manifest.json(これも触らない)

--下記2つを追加--

  • style.css
  • app.js

こんなのを作れればいいなぁと思ってます。

スクリーンショット 2019-12-23 19.04.58.png

テトリスのコードはすごく長くなるのでザーッとみてコピペでいいですw
追々細かい説明を書いていきます笑(2020年になってから笑)

テトリスのコードは下記の通りです。

index.html

先ほど作ったindex.htmlに追記(上書き)していきましょう

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="style.css">
    <link rel="manifest" href="manifest.json">
    <title>PWA Sample</title>
  </head>
  <body>
    <div class="title-wrap">
      <h1>テトリスで遊ぼう!</h1>
    </div>
    <div class="wrapper-container">
      <div class="tetris-container">
        <div class="tetris-main">
          <canvas id="stage" width="250px" height="500px" style="background-color:black;">
          </canvas>
        </div>
        <div class="tetris-option">
          <span class="tetris-panel-container">
            <p>Next</p>
              <canvas id="next" width="150px" height="150px" style="background-color:black;">
              </canvas>
            <p>LINES:<span id="lines">0</span></p>
            <p><span id="message"></span></p>
            <div class="tetris-panel-container-padding">
            <table class="tetris-button-panel">
              <tr>
                <td></td>
                <td id="tetris-rotate-button" class="tetris-button"></td>
                <td></td>
              </tr>
              <tr>
                <td id="tetris-move-left-button"class="tetris-button"></td>
                <td id="tetris-fall-button"class="tetris-button"></td>
                <td id="tetris-move-right-button"class="tetris-button"></td>
              </tr>
            </table>
            </div>
          </span>
        </div>
      </div>
    </div>
    <script src="app.js"></script>
    <script>
      var tetris = new Tetris();
      tetris.startGame();
    </script>
  </body>
</html>

style.css

style.css
html {
    touch-action: manipulation;
}



h1 {
    margin:  0;                 
    position:  relative;        
    font-size: 40px;            
    text-align:  center;        
    margin:  20px 0;            
    display: inline-block;     
}

p {
    margin:  0;                 
    line-height: 2;             
}

 .title1-wrap {
    padding-bottom:  20px;      
    text-align:  center;        
}


.wrapper-container {
    display: inline-block;
}

.tetris-container {
    height:530px;
    display: flex;
    flex-direction: row;
    margin: 10px;
    background-color: #333333;
}

.tetris-panel-contaizner {
    display: flex;
    padding-left: 10px;
    padding-right: 10px;
    flex-direction: column;
    color: white;
    background-color: #333333;
}

.tetris-panel-container-padding {
    flex-grow: 1;
}

.tetris-panel-container p {

   margin-left:20px;
   padding: 10px 0;
   font-family: sans-serif;
   font-size: 20px;
   color: #ffffff;
}

.tetris-button-panel {
    border-style: none;
    width: 100%;
    padding-top:100px;
}

.tetris-button {
    padding-top: 10px;
    padding-bottom: 10px;
    text-align: center;
    background: #444444;
    box-shadow: inset 0 2px 0 rgba(255,255,255,0.2), inset 0 -2px 0 rgba(0, 0, 0, 0.05), 0 2px 6px rgba(0, 0, 0, .15);
    border-radius: 4px;
}

.tetris-button:active {

    box-shadow: 0 0 2px rgba(0, 0, 0, 0.30);
}

app.js

app.js
class Tetris {
    constructor() {
        this.stageWidth = 10;
        this.stageHeight = 20;
        this.stageCanvas = document.getElementById("stage");
        this.nextCanvas = document.getElementById("next");
        let cellWidth = this.stageCanvas.width / this.stageWidth;
        let cellHeight = this.stageCanvas.height / this.stageHeight;
        this.cellSize = cellWidth < cellHeight ? cellWidth : cellHeight;
        this.stageLeftPadding = (this.stageCanvas.width - this.cellSize * this.stageWidth) / 2;
        this.stageTopPadding = (this.stageCanvas.height - this.cellSize * this.stageHeight) ;
        this.blocks = this.createBlocks();
        this.deletedLines = 0;

        window.onkeydown = (e) => {
            if (e.keyCode === 37) {
                this.moveLeft();
            } else if (e.keyCode === 38) {
                this.rotate();
            } else if (e.keyCode === 39) {
                this.moveRight();
            } else if (e.keyCode === 40) {
                this.fall();
            }
        }

        document.getElementById("tetris-move-left-button").onmousedown = (e) => {
            this.moveLeft();
        }
        document.getElementById("tetris-rotate-button").onmousedown = (e) => {
            this.rotate();
        }
        document.getElementById("tetris-move-right-button").onmousedown = (e) => {
            this.moveRight();
        }
        document.getElementById("tetris-fall-button").onmousedown = (e) => {
            this.fall();
        }
    }

    createBlocks() {
        let blocks = [
            {
                shape: [[[-1, 0], [0, 0], [1, 0], [2, 0]],
                        [[0, -1], [0, 0], [0, 1], [0, 2]],
                        [[-1, 0], [0, 0], [1, 0], [2, 0]],
                        [[0, -1], [0, 0], [0, 1], [0, 2]]],
                color: "rgb(0, 255, 255)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 128, 128)"
            },
            {
                shape: [[[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]],
                        [[0, 0], [1, 0], [0, 1], [1, 1]]],
                color: "rgb(255, 255, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 128, 0)"
            },
            {
                shape: [[[0, 0], [1, 0], [-1, 1], [0, 1]],
                        [[-1, -1], [-1, 0], [0, 0], [0, 1]],
                        [[0, 0], [1, 0], [-1, 1], [0, 1]],
                        [[-1, -1], [-1, 0], [0, 0], [0, 1]]],
                color: "rgb(0, 255, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 128, 0)"
            },
            {
                shape: [[[-1, 0], [0, 0], [0, 1], [1, 1]],
                        [[0, -1], [-1, 0], [0, 0], [-1, 1]],
                        [[-1, 0], [0, 0], [0, 1], [1, 1]],
                        [[0, -1], [-1, 0], [0, 0], [-1, 1]]],
                color: "rgb(255, 0, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 0, 0)"
            },
            {
                shape: [[[-1, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [1, -1], [0, 0], [0, 1]],
                        [[-1, 0], [0, 0], [1, 0], [1, 1]],
                        [[0, -1], [0, 0], [-1, 1], [0, 1]]],
                color: "rgb(0, 0, 255)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(0, 0, 128)"
            },
            {
                shape: [[[1, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [0, 0], [0, 1], [1, 1]],
                        [[-1, 0], [0, 0], [1, 0], [-1, 1]],
                        [[-1, -1], [0, -1], [0, 0], [0, 1]]],
                color: "rgb(255, 165, 0)",
                highlight: "rgb(255, 255, 255)",
                shadow: "rgb(128, 82, 0)"
            },
            {
                shape: [[[0, -1], [-1, 0], [0, 0], [1, 0]],
                        [[0, -1], [0, 0], [1, 0], [0, 1]],
                        [[-1, 0], [0, 0], [1, 0], [0, 1]],
                        [[0, -1], [-1, 0], [0, 0], [0, 1]]],
                color: "rgb(255, 0, 255)",
                highlight: "rgb(255, 255, 255)",111111111111
                shadow: "rgb(128, 0, 128)"
            }
        ];
        return blocks;
    }

    drawBlock(x, y, type, angle, canvas) {
        let context = canvas.getContext("2d");
        let block = this.blocks[type];
        for (let i = 0; i < block.shape[angle].length; i++) {
            this.drawCell(context,
                     x + (block.shape[angle][i][0] * this.cellSize),
                     y + (block.shape[angle][i][1] * this.cellSize),
                     this.cellSize,
                     type);
        }
    }

    drawCell(context, cellX, cellY, cellSize, type) {
        let block = this.blocks[type];
        let adjustedX = cellX + 0.5;
        let adjustedY = cellY + 0.5;
        let adjustedSize = cellSize - 1;
        context.fillStyle = block.color;
        context.fillRect(adjustedX, adjustedY, adjustedSize, adjustedSize);
        context.strokeStyle = block.highlight;
        context.beginPath();
        context.moveTo(adjustedX, adjustedY + adjustedSize);
        context.lineTo(adjustedX, adjustedY);
        context.lineTo(adjustedX + adjustedSize, adjustedY);
        context.stroke();
        context.strokeStyle = block.shadow;
        context.beginPath();
        context.moveTo(adjustedX, adjustedY + adjustedSize);
        context.lineTo(adjustedX + adjustedSize, adjustedY + adjustedSize);
        context.lineTo(adjustedX + adjustedSize, adjustedY);
        context.stroke();
    }

    startGame() {
        let virtualStage = new Array(this.stageWidth);
        for (let i = 0; i < this.stageWidth; i++) {
            virtualStage[i] = new Array(this.stageHeight).fill(null);
        }
        this.virtualStage = virtualStage;
        this.currentBlock = null;
        this.nextBlock = this.getRandomBlock();
        this.mainLoop();
    }

    mainLoop() {
        if (this.currentBlock == null) {
            if (!this.createNewBlock()) {
                return;
            }
        } else {
            this.fallBlock();
        }
        this.drawStage();
        if (this.currentBlock != null) {
            this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
                this.stageTopPadding + this.blockY * this.cellSize,
                this.currentBlock, this.blockAngle, this.stageCanvas);
        }
        setTimeout(this.mainLoop.bind(this), 500);
    }

    createNewBlock() {
        this.currentBlock = this.nextBlock;
        this.nextBlock = this.getRandomBlock();
        this.blockX = Math.floor(this.stageWidth / 2 - 2);
        this.blockY = 0;
        this.blockAngle = 0;
        this.drawNextBlock();
        if (!this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, this.blockAngle)) {
            let messageElem = document.getElementById("message");
            messageElem.innerText = "GAME OVER";
            return false;
        }
        return true;
    }

    drawNextBlock() {
        this.clear(this.nextCanvas);
        this.drawBlock(this.cellSize * 2, this.cellSize, this.nextBlock,
            0, this.nextCanvas);
    }

    getRandomBlock() {
        return  Math.floor(Math.random() * 7);
    }

    fallBlock() {
        if (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
            this.blockY++;
        } else {
            this.fixBlock(this.blockX, this.blockY, this.currentBlock, this.blockAngle);
            this.currentBlock = null;
        }
    }

    checkBlockMove(x, y, type, angle) {
        for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
            let cellX = x + this.blocks[type].shape[angle][i][0];
            let cellY = y + this.blocks[type].shape[angle][i][1];
            if (cellX < 0 || cellX > this.stageWidth - 1) {
                return false;
            }
            if (cellY > this.stageHeight - 1) {
                return false;
            }
            if (this.virtualStage[cellX][cellY] != null) {
                return false;
            }
        }
        return true;
    }

    fixBlock(x, y, type, angle) {
        for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
            let cellX = x + this.blocks[type].shape[angle][i][0];
            let cellY = y + this.blocks[type].shape[angle][i][1];
            if (cellY >= 0) {
                this.virtualStage[cellX][cellY] = type;
            }
        }
        for (let y = this.stageHeight - 1; y >= 0; ) {
            let filled = true;
            for (let x = 0; x < this.stageWidth; x++) {
                if (this.virtualStage[x][y] == null) {
                    filled = false;
                    break;
                }
            }
            if (filled) {
                for (let y2 = y; y2 > 0; y2--) {
                    for (let x = 0; x < this.stageWidth; x++) {
                        this.virtualStage[x][y2] = this.virtualStage[x][y2 - 1];
                    }
                }
                for (let x = 0; x < this.stageWidth; x++) {
                    this.virtualStage[x][0] = null;
                }
            let linesElem = document.getElementById("lines");
                this.deletedLines++;
                linesElem.innerText = "" + this.deletedLines;
            } else {
                y--;
            }
        }
    }

    drawStage() {
        this.clear(this.stageCanvas);

        let context = this.stageCanvas.getContext("2d");
        for (let x = 0; x < this.virtualStage.length; x++) {
            for (let y = 0; y < this.virtualStage[x].length; y++) {
                if (this.virtualStage[x][y] != null) {
                    this.drawCell(context,
                        this.stageLeftPadding + (x * this.cellSize),
                        this.stageTopPadding + (y * this.cellSize),
                        this.cellSize,
                        this.virtualStage[x][y]);
                }
            }
        }
    }

    moveLeft() {
        if (this.checkBlockMove(this.blockX - 1, this.blockY, this.currentBlock, this.blockAngle)) {
            this.blockX--;
            this.refreshStage();
        }
    }

    moveRight() {
        if (this.checkBlockMove(this.blockX + 1, this.blockY, this.currentBlock, this.blockAngle)) {
            this.blockX++;
            this.refreshStage();
        }
    }

    rotate() {
        let newAngle;
        if (this.blockAngle < 3) {
            newAngle = this.blockAngle + 1;
        } else {
            newAngle = 0;
        }
        if (this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, newAngle)) {
            this.blockAngle = newAngle;
            this.refreshStage();
        }
    }

    fall() {
        while (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
            this.blockY++;
            this.refreshStage();
        }
    }

    refreshStage() {
      this.clear(this.stageCanvas);
      this.drawStage();
      this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
                this.stageTopPadding + this.blockY * this.cellSize,
                this.currentBlock, this.blockAngle, this.stageCanvas);
    }

    clear(canvas) {
        let context = canvas.getContext("2d");
        context.fillStyle = "rgb(0, 0, 0)";
        context.fillRect(0, 0, canvas.width, canvas.height);
    }
}

githubで公開する!!

では作成したファイルをサーバーに公開して終了となります。
サーバーはhttpsが利用できるAWSやherokuなどで頑張って公開しようかと思っていたところ、
githubだけで簡単にwebページを公開する方法があるそうです。

しかもgithubの公式さんが言うには、かなり簡単とのこと。
これはやるしかないですね:raised_hands_tone3:

github公式が割とわかりやすいのでおいておきますね。
https://pages.github.com/

github.ioと言うものらしいです。

github.ioのメリットデメリット

メリット

  • 簡単にwebページを公開できる
  • 無料
  • 更新したい場合はリポジトリにpushするだけ

デメリット

  • ソースコードが公開される
  • サーバー依存のjsとかは使えない

今回は...

  • ただ自分で作ったwebページを公開するだけ(特にセキリュティを気にするものはない)
  • jsはサーバーに依存しない

と言うことで最適な方法と言えるのではないのでしょうか。

方法

やり方は簡単で、
リポジトリ名を
username.github.io
にするだけでいいらしいです。すごい。

ではやってみましょう。(本当にほぼ公式のやり方をなぞっていくだけですw)

① まずはリポジトリを作ります。
命名規則はusername.github.io だそうです。
僕の場合は下記のようになります。

スクリーンショット 2019-12-23 13.24.24.png

② 次に下記のコマンドを実行します。デスクトップとかでね

クローンして

git clone https://github.com/username/username.github.io

移動してサンプルテキストを入れて(index.htmlを作って)

cd username.github.io

echo "Hello World" > index.html
git add --all

git commit -m "Initial commit"

git push -u origin master

そして https://username.github.io にアクセスすればHelloWorldと表示されるはず!

スクリーンショット 2019-12-23 13.48.09.png

:astonished: :astonished: :astonished: :astonished:
なぜか404と怒られてしまいました。。。

原因はよくわかりませんw
ですが作ったリポジトリのsettings にある
スクリーンショット 2019-12-23 13.50.54.png
ここにリンクが載っていました。
リンクの構造がgithub公式の構造と少し違いますが、とりあえず踏んでみましょう。
スクリーンショット 2019-12-23 13.52.55.png

表示されました!
(反映されるまで、少し時間が(30秒くらい)かかるかもしれないので焦らずに待ちましょう)

これで作ったゲームを公開する準備はできました。

このリポジトリにテトリスのファイルをぶち込んで完成となります。

動作確認

最終的なファイルは
- index.html
- style.css
- app.js
- manifest.json
- service_worker.js

の5つになると思います。

githubのリポジトリに全てのファイルが揃ったらhttps://username.github.ioにアクセスしてみましょう。
PC、スマホ両方でアクセスすると良いと思います。

SmartSelect_20191223-185402_One UI Home.jpg

こんな感じでgooglechoromeとは別のタブで開けていたらOKです!
オフラインでの動作を確認したいので一度タブを閉じで機内モードにしてから開いてみてください:information_desk_person:
機内モードでもテトリスで遊べちゃうと思います!
恐るべしPWA。。。

こんな感じで、テトリスをスマホアプリのように扱えるようになりました。
リンクを友達に教えると友達のスマホでも遊べるようになるのでぜひ友達に送りつけてみてください。

参照記事(結構コピペしてますw):bow_tone2:

https://paiza.hatenablog.com/entry/2018/08/29/PWA%E5%85%A5%E9%96%80%EF%BC%81JavaScript%E3%81%A7%E7%B0%A1%E5%8D%98%E3%81%AB%E9%AB%98%E9%80%9F%E5%8C%96%EF%BC%86%E3%82%AA%E3%83%95%E3%83%A9%E3%82%A4%E3%83%B3%E5%AF%BE%E5%BF%9C%E3%81%AEWeb%E3%82%B5

http://kmaebashi.com/programmer/tetris/index.html

https://www.webprofessional.jp/what-is-the-attention-project-pwa-by-google/

https://www.seohacks.net/basic/terms/pwa/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VisualStudioCodeでタグをクリックした際にマルチカーソルになってしまう時の対処法(HTML)

Visual Studio Code(以下VS Code)でHTMLを編集しようとしたところ、突然開始タグと終了タグの両方にカーソルが入ってしまう様になりました。
なんでやと思っていろいろ調べていたら、どうやらVS Codeの最新版「November 2019」リリース(バージョン1.41)でそういった機能が追加されたそうです。(デフォルトで設定されてるとか余計なことを。。。)

対処法

ファイル→基本設定→設定 を開く
カーソル対処法①.png

拡張機能→HTML よりMirror Cursor On Matching Tagのチェックを外す
カーソル対処法②.png

これでタグをクリックすると片方のみカーソルが入るようになっているはずです。(何か間違ってたらごめんなさい)
いかんせん出たばっかりで情報が少なかったので、誰かしら役に立ったらいいかなーと思います。(あと半分備忘録的な感じ)

以上、Qiita初投稿でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

hamlインストールエラー

実行したい内容

bundleしてhtmlをhamlにしたい。

起こっている現象

Gemfile最下部にgem "haml-rails", "~> 2.0"と記述しbundleを行うがエラーとなる

エラー内容

ターミナル
MacBook-Air:a PC$ bundle
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for
--中省略--

Fetching mysql2 0.5.3
Installing mysql2 0.5.3 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

--中省略--
-----
checking for mysql.h... yes
checking for errmsg.h... yes
checking for SSL_MODE_DISABLED in mysql.h... no
checking for MYSQL_OPT_SSL_ENFORCE in mysql.h... no
checking for MYSQL.net.vio in mysql.h... yes
checking for MYSQL.net.pvio in mysql.h... no
checking for MYSQL_ENABLE_CLEARTEXT_PLUGIN in mysql.h... yes
checking for SERVER_QUERY_NO_GOOD_INDEX_USED in mysql.h... yes
checking for SERVER_QUERY_NO_INDEX_USED in mysql.h... yes
checking for SERVER_QUERY_WAS_SLOW in mysql.h... yes
checking for MYSQL_OPTION_MULTI_STATEMENTS_ON in mysql.h... yes
checking for MYSQL_OPTION_MULTI_STATEMENTS_OFF in mysql.h... yes
checking for my_bool in mysql.h... yes
-----
Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load
-----
-----
Setting libpath to /usr/local/opt/mysql@5.6/lib
-----
creating Makefile

current directory: /Users/itsumi/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.5.3/ext/mysql2
make "DESTDIR=" clean

current directory: /Users/itsumi/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.5.3/ext/mysql2
make "DESTDIR="
compiling client.c
compiling infile.c
compiling mysql2_ext.c
compiling result.c
compiling statement.c
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2

Gem files will remain installed in /Users/itsumi/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.5.3 for inspection.
Results logged to /Users/itsumi/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-18/2.5.0-static/mysql2-0.5.3/gem_make.out

An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  mysql2

この部分がエラー内容で重要そう
``
An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue.
Make sure that
gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
mysql2
```

色々サイト検索を行い、下記にたどり着く

打ち込む

ターミナル
$ brew info openssl

出てきた内容

ターミナル
export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"

再び打ち込む

ターミナル
$gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/' -- --with-cppflags=-I/usr/local/opt/openssl@1.1/include --with-ldflags=-L/usr/local/opt/openssl@1.1/lib

出てきた内容

ターミナル
1 gem installed

再び試す

ターミナル
$bundle

エラーなし!!

通常通りターミナルで$ rails haml:erb2haml実行
ターミナル
Would you like to delete the original .erb files? (This is not recommended unless you are under version control.) (y/n)
#yを入力してエンター

haml変換成功!

お世話になったページたち

mysql2 gemインストール時のトラブルシュート
【Rails】MySQL2がbundle installできない時の対応方法
RailsプロジェクトでMySQLがbundle installできなかった
bundle install 時、mysql2でエラー

終わりに

チーム開発スタート初っ端の出来事。
いきなりのエラーです。自分一人じゃないから適当にできない。。。
最初のうちに経験できてよかった(が、エラーはまだまだ続く予感。笑)
完璧ではなくていいから完成させるように頑張ります!
(bundle exec installも試してみればよかったと今更ながら思う←)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よく使う カラムレイアウトのマークアップ

めちゃめちゃよく使うので、今更ながらメモに残しとく?

左右のmarginは、.listItem + .listItemで、
上下のmarginは、2段目から取るようにしています。
左に左に、上に上に、marginをつけます。

こんな感じ↓
名称未設定ファイル (1).png

PCで2カラム、SPで1カラム

名称未設定ファイル (2).png

index.html
<ul class="col2List">
  <li class="col2ListItem"></li>
  <li class="col2ListItem"></li>
  <li class="col2ListItem"></li>
  <li class="col2ListItem"></li>
  <li class="col2ListItem"></li>
</ul>
style.scss
* {
  margin: 0;
  padding: 0;
}
.col2List {
  display: flex;
  flex-wrap: wrap;
  @media (max-width: 640px) {
    display: block;
  }
  .col2ListItem {
    width: 48%; // デザインにより変更
    @media (max-width: 640px) {
      width: 100%;
    }
  }
}
.col2ListItem + .col2ListItem {
  margin-left: 4%; // デザインにより変更
  @media (max-width: 640px) {
    margin-left: 0;
    margin-top: 7vw; // デザインにより変更
  }
}
@media (min-width: 641px) {
  .col2ListItem:nth-child(n + 3) {
    margin-top: 4.8%; // デザインにより変更
  }
  .col2ListItem:nth-child(2n + 1) {
    margin-left: 0;
  }
}

PCで3カラム、SPで1カラム

名称未設定ファイル.png

index.html
<ul class="col3List">
  <li class="col3ListItem"></li>
  <li class="col3ListItem"></li>
  <li class="col3ListItem"></li>
  <li class="col3ListItem"></li>
  <li class="col3ListItem"></li>
</ul>
style.scss
* {
  margin: 0;
  padding: 0;
}
.col3List {
  display: flex;
  flex-wrap: wrap;
  @media (max-width: 640px) {
    display: block;
  }
  .col3ListItem {
    width: 30%; // デザインにより変更
    @media (max-width: 640px) {
      width: 100%;
    }
  }
}
.col3ListItem + .col3ListItem {
  margin-left: 5%; // デザインにより変更
  @media (max-width: 640px) {
    margin-left: 0;
    margin-top: 7vw; // デザインにより変更
  }
}
@media (min-width: 641px) {
  .col3ListItem:nth-child(n + 4) {
    margin-top: 4.8%; // デザインにより変更
  }
  .col3ListItem:nth-child(3n + 1) {
    margin-left: 0;
  }
}

PCで4カラム、SPで1カラム

名称未設定ファイルのコピー.png

index.html
<ul class="col4List">
  <li class="col4ListItem"></li>
  <li class="col4ListItem"></li>
  <li class="col4ListItem"></li>
  <li class="col4ListItem"></li>
  <li class="col4ListItem"></li>
  <li class="col4ListItem"></li>
</ul>
style.scss
* {
  margin: 0;
  padding: 0;
}
.col4List {
  display: flex;
  flex-wrap: wrap;
  @media (max-width: 640px) {
    display: block;
  }
  .col4ListItem {
    width: 23%; // デザインにより変更
    @media (max-width: 640px) {
      width: 100%;
    }
  }
}
.col4ListItem + .col4ListItem {
  margin-left: 2.5%; // デザインにより変更
  @media (max-width: 640px) {
    margin-left: 0;
    margin-top: 7vw; // デザインにより変更
  }
}
@media (min-width: 641px) {
  .col4ListItem:nth-child(n + 5) {
    margin-top: 2.5%; // デザインにより変更
  }
  .col4ListItem:nth-child(4n + 1) {
    margin-left: 0;
  }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ancestry'使用時,seedsに多階層カテゴリを書くのめんどくさいから、一気に置換してみる

gem 'ancestry'使用時,seedsに多階層カテゴリを書くのめんどくさい

一階層でこの分量!孫の階層までペタペタ書くのめんどくさい。
少しでも楽したい。せめてexcelみたいに一気に置換とかしてらくしたい。

image.png

image.png


 VSCodeでコピペでなんとかしてみる。

とりあえず一階層目を、検証ツールでコピペする。(どうせ加工するので、適当でOK。)
image.png


command + F → ▷ をおす → .* を押す

image.png


<option value="1">レディース</option>

↓ こんな風に一気にかえたい!

{layer1:"レディース",layer1_chaild:[]
{layer1:"メンズ",layer1_chaild:[]
{layer1:"インテリア・住まい・小物",layer1_chaild:[]
{layer1:"本・音楽・ゲーム",layer1_chaild:[]
{layer1:"おもちゃ・ホビー・グッズ",layer1_chaild:[]
{layer1:"コスメ・香水・美容",layer1_chaild:[]
{layer1:"家電・スマホ・カメラ",layer1_chaild:[]
{layer1:"スポーツ・レジャー",layer1_chaild:[]
{layer1:"ハンドメイド",layer1_chaild:[]
{layer1:"チケット",layer1_chaild:[]
{layer1:"自動車・オートバイ",layer1_chaild:[]
{layer1:"その他",layer1_chaild:[]


作業1

検索に↓を入力

<option value="\d+">

\d+ → これは数列をさしてます。

置換に↓を入力

{layer1:"


作業2

検索に↓を入力

</option>

置換に↓を入力

",layer1_chaild:[]\n

\n → 改行です。


ポイント

ワイルドカードの使い方がEXCELとは勝手がちがう。。。

正規表現を使用する必要があるみたいです。
Visual Studio での正規表現の使用

この要領で、2階層と3階層も作成できると思います。

この方法でも、やっぱめんどくさいので、もっといい方法がありそうです。
あと、カテゴリー一覧ページみたいなのがあったら、スクレイピングとかしても面白いかも。(訳あって今回はスクレイピングはできなかったけど。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヘッダーを作るときに考えること

Webサイトを訪問した際、一番初めに目に入るものとはなんでしょうか。

目を惹くメインビジュアル?仰々しいキャッチコピー?
リッチなサイトならばローディング画面?

サイトの構成によってその答えも異なりますが、ほとんどの場合、
そうした注目ポイントに付随するように ヘッダー の存在があるのではないでしょうか。

この文章を読んでいる頃ならまだ、最近新しくなったQiitaロゴを有した黄緑色のヘッダーが画面上部に拝めるのではないかと思います。スマホの方は少しだけ上にスクロール。

サイト共通のヘッダーとは、ある種サイトの名刺のようなものであり、サイトの回遊を助けるナビゲーターであり、常にユーザーに寄り添うような(最近は追従するヘッダーも多いですね)、甘くてクリーミーな特別な存在(パーツ)なのです。

Webサイトを構築するうえで、(この言葉は苦手なのですが) 必須 ともいえるヘッダー。

そんなヘッダーを作るときにHTMLをどう書くかについて考えます。

サンプルケース

サンプルとして特に装り気もない 一般的な ヘッダーをつくります。

一般的ってなんじゃいという話なのですが、

  • タイトル兼トップに戻るリンクゾーン
  • ナビゲーションリストゾーン
  • ハンバーガーメニューボタン

最低限これらさえあればヘッダーとして名乗りを上げることはできるでしょう。

クラスに関してはわかりやすいようBEMライクに書くとして、ひとまずアウトラインを作成します。

header.html
<header class="header">
  <!-- タイトル兼トップに戻るリンクゾーン -->
  <h1 class="header_title">
    <a href="/">サイトタイトル</a>
  </h1>
  <!-- ナビゲーションリストゾーン -->
  <nav class="header_nav">
    <ul class="header_navList">
      <li><a href="#section01">項目01</a></li>
      <li><a href="#section02">項目02</a></li>
      <li><a href="#section03">項目03</a></li>
    </ul>
  </nav>
  <!-- ハンバーガーメニューボタン -->
  <button type="button" class="header_btn"></button>
</header>

う〜んセマンティック。見事にヘッダーの材料が揃っていますね。
(ハンバーガーメニューに関してはコードが長くなるため今回は省略します)

とはいえCSSを当てていない状態ではヘッダーもどきにすらなりきれていません。
早速調理を開始したいところなのですが、このHTMLには欠けているものがあります

<div> がひとつもないのです。

<header>は<div>の代わりではない

これは <header> に限らず、HTML5においてよく言われることですね。
<section><nav> のようにHTMLのアウトラインを生成するわけではない……つまりセクショニングコンテンツではないとされているため、 <header> で囲む意味としては「ここがヘッダーですよ〜」という目印程度のものと考えられます(それが重要ではあるのですが)。

ただ、今のコードではその目印である <header class="header"> しか、色をつけたり引き伸ばしたりする対象がありません。

header.css
.header {
  display: flex;
  align-items: center;
  background: #ccc;
  width: 100%;
  height: 60px;
  /* etc... */
}

こんな感じでほとんどのCSSが一点に集中せざるを得なくなります。
ここから「背景は画面いっぱいに、だけど内部の最大幅を1000pxに収めたい」とかしようとすると、えっこれ無理では?……となって、結局 <div> でcontainerやinnerを書くことになります。
そんなことなら最初からこうしましょう。

header.html
<header class="header">
  <div class="header_container">
    <div class="header_inner">
      <!-- 中身は省略 -->
    </div><!-- /.header_inner -->
  </div><!-- /.header_container -->
</header>
header.css
.header {
/*   display: flex;
  align-items: center;
  background: #ccc;
  width: 100%;
  height: 60px; */
}

.header_container {
  background: #ccc;
  width: 100%;
  height: 60px;
}

.header_inner {
  display: flex;
  align-items: center;
  margin: auto;
  max-width: 1000px;
}

ネストは増えましたが、こんな感じに分割した方が後々の修正もしやすそうですね。
<header> にいたってはCSSが空っぽになってしまいましたが、「<header>は<div>の代わりではない」ので、<header>を修飾するためには別の <div> が必要だった と考えるとよさげです。

そもそも、 <header class="header"> という書き方も、今となってはもにょります。
まるでHTML5すら知らなかった幼き日に書いた <div class="header">置換しただけ のようで……。
(BEMのルール的には仕方なさそうなのですが)

<section>も<div>の代わりにしない方がいい

先ほどの修正によって、 <header> に何もCSSが当たらなくなったように、個人的にはセクショニングコンテンツである <section> にも直接CSSをあらかじめ当てておくのはやめた方がいいのではないかと考えています。

通常の場合は特に問題ないのですが、例えば先ほどのヘッダーがスクロールに合わせて追従する……。
いわゆる固定ヘッダーになった場合、position: fixed; を付けることでヘッダーを浮かせて固定します。

header.css
.header {
  position: fixed;
}

浮いたヘッダーに後に続くメインコンテンツが隠れないよう、ヘッダーの高さ分(今回は 60px)ずらす必要があるのですが、もし固定ヘッダーに各セクションへのアンカーリンク <a href="#~~~"> がある場合、アンカーの到着する位置も同じだけズラす必要があります。

この問題を解消するCSSが以下なのですが、

section.css
section {
  margin-top: -60px;
  padding-top: 60px;
}

もしこれを background と併用してしまうと、padding-top の分だけ伸びた背景が margin-top のネガティブマージン分だけ上にズレることになるため、本来の位置よりもヘッダーの高さ分上にズレて見える <section> が爆誕します(そもそも marginpadding を当てている場合もそれらを上書きしてしまいますね)。

これを防ぐためには先ほどのヘッダーのように、内に身代わりの <div> をかますことが大事です。

section.html
<section id="#section01">
  <div class="section_container">
    <div class="section_inner">
      <!-- 中身は省略 -->
    </div><!-- /.section_inner -->
  </div><!-- /.section_container -->
</section>
section.css
section {
  margin-top: -60px;
  padding-top: 60px;
}

.section_container {
  background: #000000;
  /* ... */
}

他にも考えることは色々とあるのですが、HTMLをシンプルにしすぎるのもほどほどにした方がよさそうです。

おまけ

実ははじめに見ていただいたQiitaのヘッダーのガワ部分も、
<div class="st-HeaderContainer">
とガッツリ <div> だったりします。
(グローバルフッターは <footer> だったので、なにか意図があるのかないのか分かりませんが)

あらためて、タグの定義された意図を再解釈する必要があると感じる年末なのでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

progate html,css道場 2

hello world.の"."を赤くする

文中の一部にCSSを適用させたい場合
タグを使う。

spanは改行されない。

h1,pタグは改行される

<h1>aa<p>a</p></h1>

aa

a

<h1>aa<span>a</span></h1>

aaa

あとは、spanタグにCSS適用して終わり!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

progate html,css道場 1

HTML&CSS 道場 初級編で詰まったところ

liリストを横に広げる

改善前コード

index.html
<header>
<div class= "header">
  <div class="logo">Progate</div>
    <div class="header-menu">
      <ul>
        <li>プログラミングとは</li>
        <li>学べるレッスン</li>
        <li>お問い合わせ</li>
      </ul>
    </div>
  </div>
</header>
stylesheet.css
header {
  height: 90px;
  width: 100%;
  background-color: #26d0c9;
}

header .header-logo {
  font-size: 36px;
  padding: 20px 40px;
  color: #fff;
  float: left;
}

header .header-menu {
  padding: 33px 20px;
  color: #fff;
  float: left; 
}

メニューリストを縦から横へ改善する

<div class="header-menu">
header .header-menu {
  padding: 33px 20px;
  color: #fff;
  float: left; 
}

メニューに指定したクラスに行うのではなく
liタグに対して 変更をかける

header li {
  padding: 33px 20px;
  color: #fff;
  float: left; 
}

なぜ?
floatはpaddingを無効化する?
とか言ってたかも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

可愛いしろくまの404ページを作ってみた

WordPressの自作テーマを作成中、404ページをちょっと変わったデザインにしてみようと思い、404の「0」の部分をしろくまに変えたデザインを作ってみました。

404ページに行くのが楽しくなる!...かも?

実際に作った、しろくまくん入りの404ページ

こちらが実際にWordPressテーマ作成中に作った、しろくまくん入りの404ページになります。
ページが見つからなかった。という悲しさを表現する為に泣かせています( ;∀;)

ちなみに雪の結晶以外は全て、HTMLとCSSのみで作りました!
9a0f6d606ffec1e45a87a4f929c807ae.gif

書いたコード

※スマホで見るとぐしゃぐしゃになってしまいます(`;ω;´)

全体のコード

See the Pen 404Shirokuma by yuma11 (@yuma11) on CodePen.

しろくまの部分

See the Pen Shirokuma by yuma11 (@yuma11) on CodePen.

コードの解説

shirokuma.scss
.kuma { // しろくまの背景
  position: relative;
  margin: 0 auto;
  width: 320px;
  height: 320px;
  border-radius: 50%;
  background: #3c3c3b;
  &__ear { // 耳
    position: absolute;
    top: 50px;
    width: 60px;
    height: 60px;
    border-radius: 80px 100px 0 100px / 100px 130px 0 100px;
    background: #fff;
    &--left { // 左耳
      left: 60px;
      &:after { // 左耳の穴
        position: absolute;
        top: 12px;
        left: 12px;
        width: 40px;
        height: 40px;
        border-radius: 80px 100px 0 100px / 100px 130px 0 100px;
        background: #000;
        content: '';
      }
    }
    &--right { // 右耳
      right: 60px;
      transform: scale(-1, 1);
      &:after { // 右耳の穴
        position: absolute;
        top: 12px;
        right: 8px;
        width: 40px;
        height: 40px;
        border-radius: 80px 100px 0 100px / 100px 130px 0 100px;
        background: #000;
        content: '';
      }
    }
  }
  &__face { // 顔
    position: absolute;
    bottom: 0;
    left: 50%;
    width: 160px;
    height: 250px;
    border-radius: 80px 80px 0 0;
    background: #fff;
    transform: translateX(-50%);
    &:before { // 顔の左下部分(斜めになっている場所)
      position: absolute;
      bottom: 20px;
      left: -5px;
      width: 20px;
      height: 50px;
      background: white;
      content: '';
      transform: skewX(-20deg);
    }
    &:after { // 顔の右下部分(斜めになっている場所)
      position: absolute;
      right: -5px;
      bottom: 20px;
      width: 20px;
      height: 50px;
      background: white;
      content: '';
      transform: skewX(20deg);
    }
  }
  &__eye { // 目
    position: absolute;
    top: 130px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #000;
    transform-origin: 100px 200px;
    animation: Cry 3s infinite;
    &--left { // 左目
      left: 120px;
      &:after { // 左目の中身(白い部分)
        position: absolute;
        top: 50%;
        left: 50%;
        width: 7px;
        height: 7px;
        border-radius: 50%;
        background: #fff;
        content: '';
        transform: translateX(-50%);
      }
    }
    &--right { // 右目
      right: 120px;
      &:after { // 右目の中身(白い部分)
        position: absolute;
        top: 50%;
        left: 50%;
        width: 7px;
        height: 7px;
        border-radius: 50%;
        background: #fff;
        content: '';
        transform: translateX(-50%);
      }
    }
  }
  &__tears { // 涙
    position: absolute;
    top: 145px;
    right: 125px;
    width: 8px;
    height: 15px;
    border-radius: 24px 24px 24px 24px / 63px 63px 24px 24px;
    background: skyblue;
    opacity: 0;
    animation: Tears 3s infinite;
  }
  &__nose { // 鼻
    position: absolute;
    top: 150px;
    left: 50%;
    width: 30px;
    height: 20px;
    border-radius: 40% 40% 55% 55%;
    background: #000;
    transform: translateX(-50%);
    &--bottom { // 鼻の下(縦に伸びている線)
      position: absolute;
      top: 150px;
      left: 50%;
      width: 3px;
      height: 30px;
      background: #000;
      transform: translateX(-50%);
    }
  }
  &__mouth { // 口
    position: absolute;
    top: 174px;
    width: 20px;
    height: 20px;
    border-width: 0 3px 3px 0;
    border-style: solid;
    border-color: #000;
    border-radius: 0 0 20px 0 / 0 0 20px 0;
    &--left { // 口の左側
      left: 135px;
      transform: rotate(25deg);
    }
    &--right { // 口の右側
      right: 135px;
      transform: rotate(65deg);
    }
  }
}

@keyframes Cry { // まばたきをするアニメーション
  0% {
    height: 20px;
    transform: translateY(0);
  }
  25% {
    height: 0;
    transform: translateY(15px);
  }
  50% {
    height: 20px;
    transform: translateY(0);
  }
  100% {
    height: 20px;
    transform: translateY(0);
  }
}

@keyframes Tears { // 涙が流れるアニメーション
  0% {
    opacity: 0;
    transform: translateY(0);
  }
  25% {
    opacity: 1;
  }
  50% {
    opacity: 0;
    transform: translateY(20px);
  }
  100% {
    opacity: 0;
    transform: translateY(0);
  }
}

作ってみて感じたこと

CSSで何か作るのって凄く楽しい...!!
書いたコードがすぐに反映されていくから楽しくて仕方なかったです(´∇`)

まだまだ技術的に足りない部分や、知らない部分が多いので今後もスキルを高めていきたいと思います。

ちなみにしろくまくんを作っている中で、border-radiusの値を8つ指定して使うことが多かったのですが、そんな時にかなり便利なサイトを見つけました↓
FANCY-BORDER-RADIUS
こちらのサイトは直感的に形を作るだけで、その形のborder-radiusの値を出してくれる便利なサイトです。

最後までご覧いただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

かっこいい横スクロールを実装する

ちょっと個人的なメモ程度なので適当です。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>横スクロール</title>
    <style>
        html,
        body {
            margin: 0;
            width: 100%;
            height: 100%;
        }

        .main {
            width: 100%;
            height: 200px;
            overflow-x: scroll;
            display: flex;
        }

        .main::-webkit-scrollbar {
            display: none;
        }

        .card {
            width: 200px;
            height: 170px;
            margin-top: 5px;
            margin-right: 5px;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26);
            border-radius: 8px;
            flex: 0 0 auto;
            margin-left: 10px;
            background-color: #DADCE0;
            flex-shrink: 0;
        }
    </style>
</head>

<body>
    <div class="main">
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
        <div class="card">

        </div>
    </div>
</body>

</html>

PCでは使いにくいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ダークテーマとかに切り替える。

皆さんこんにちはルサカです。最近ダークテーマ流行してますよね!
今回は、ユーザーのテーマを取得したり、ユーザーがテーマを変更できるといったものを作って行きたいと思います。

まぁ作っていこう!

@media (prefers-color-scheme: light) {
  body {
    background-color: white;
    color: black;
  }
}

@media (prefers-color-scheme: dark) {
  body {
    background-color: black;
    color: white;
  }
}

っていう方法でもダークテーマとかの切り替えはできますが、
テーマを変更する処理が結構めんどくさいので、今回は、以下のコードのようにbodyタグにtheme属性を作ってテーマの切り替えを容易にします。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ダークテーマ</title>
    <link rel="stylesheet" href="style.css">
</head>
<body theme="dark">

</body>
</html>

ちなみにCSSはこのように属性ごとにCSS変数を設定しています。
CSS変数についてあまりわからないという方は
CSS カスタムプロパティ (変数) の使用(MDN)を見ると理解が深まると思います。

:root {
    --s-1: 0 2px 5px rgba(0, 0, 0, 0.26);
    --s-2: 0 2px 10px rgba(0, 0, 0, .2);
}

body[theme="dark"] {
    --m-bg: #121212;
    --d-color: #fff;
    --s-color: #9F9F9F;
    --s-bg: #242424;
    --b-1: 1px #242424 solid;
    --a-color: #EF5455;
}

body[theme="light"] {
    --m-bg: #FFF;
    --d-color: #202124;
    --s-color: #5F6368;
    --b-1: 1px solid #dadce0;
    --a-color: #EF5455;
}
@keyframes s-1 {
    0% {
        box-shadow: 0 0.001px 0.001px rgba(0, 0, 0, 0.26);
    }
    100% {
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26);
    }
}

body{
    background: var(--m-bg);
    color: var(--d-color);
}

切り替え系を作っていこう!

ここからが本番ですね!
まずHTMLにこのようなボタンを追加してください。

<button id="themechange">テーマの変更</button>

ちなみに私の場合は以下のようにボタンを装飾しました。

#themechange{
    border: var(--b-1);
    background: var(--m-bg);
    color: var(--a-color);
    font-size: 18px;
    font-weight: bold;
    padding: 10px 15px;
    border-radius: 4px;
    transition: all 1s;
}

#themechange:hover{
    border-color: var(--a-color);
    background: var(--a-color);
    color: #fff;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26);
    animation: s-1 1s ease 0s 1 alternate none running;
}
#themechange:focus{
    outline: none;
}

切り替えの処理はJavaScriptで書きます。

document.getElementById("themechange").addEventListener("click",function(){
    var theme = document.body.getAttribute("theme")
    if (theme === "dark") {
        document.body.setAttribute("theme","light")
    } else {
        document.body.setAttribute("theme","dark")
    }
})

次に、OSのテーマを取得して、テーマの初期値を設定しましょう。
コードは以下のようになります。

if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    document.body.setAttribute("theme","dark")
} else {
    document.body.setAttribute("theme","light")
}

これで基本的なテーマ切り替えが完了しました。
他にも、ユーザーがテーマを変更したらその設定をlocalstorageに保存するといったことができると思います。

デモ!

See the Pen jOEmWEW by anyfre (@anyfre) on CodePen.

まとめ

結構簡単にできました。
Twitterフォローしてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

あなたの好きなCSSプロパティは何ですか?

次の中から選んでね

  • transform: translate
  • margin: 0 auto
  • position: absolute

transformが好きなあなた

普通の人。みんな大好きtransform。
要素を変形するには万能なCSSプロパティ。

<div class="box"></div>
<div class="box box--move"></div>
.box {
  width: 100px;
  height: 100px;
  border: 1px solid red;
  box-sizing: border-box;
}

.box--move {
  transform: translate(100px, 0);
}

スクリーンショット 2019-12-22 2.46.35.png

まあこうなる。

では、これは?

<div class="box box--move-large"></div>
<div class="box box--large-move"></div>
.box--move-large {
  transform: translate(100px, 0) scale(2);
}

.box--large-move {
  transform: scale(2) translate(100px, 0);
}

スクリーンショット 2019-12-22 2.46.42.png

順番を変えただけで見え方が変わってしまった。
これには理由がある。

適用される 1 つ以上の CSS 変形関数です。変形関数は、左から右へ順に重ねられ、つまり右から左の順に変形の混合の効果が適用されます。
https://developer.mozilla.org/ja/docs/Web/CSS/transform

つまり、 2倍大きくなってからの translate(100px, 0)200px 移動することになる。

margin: 0 auto が好きなあなた

安定志向。
昔ながらの伝統的な中央寄せ。

<div class="box box--center"></div>
.box {
  width: 100px;
  height: 100px;
  border: 1px solid red;
}

.box--center {
  margin: 0 auto;
}

コンポーネント指向が浸透した昨今は

レイアウトは親要素に任せる方が適切なシーンが多い。
.box はどこに配置さるかを気にしない

<div class="container">
  <div class="box"></div>
</div>
.container {
  display: flex;
  justify-content: center;
}

position: absoluteが好きなあなた

きっと、すいも甘いも知った人。
absolute の従順さに惚れ惚れする。

<div class="container">
  <div class="box box--bottom"></div>  
</div>
.box--bottom {
  position: absolute;
  bottom: 10px;
  right: 10px;
}

.container {
  width: 80%;
  height: 300px;
  border: 1px solid blue;
  margin: 20px auto;
}

スクリーンショット-2019-12-22-3.02.49.png

親要素をたどって起点を探す

absoluteの bottom, right が基準となるのは position: relative がついている親要素。

つまり、 .containerposition: relative を設定すれば
.container を基準としたポジションとなる。

.container {
  position: relative;
}

スクリーンショット-2019-12-22-3.03.24.png

従順なやつだなと思う。

※動画解説版
https://www.youtube.com/watch?v=VFkwIGoKATo

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む