twitter bot

まう、開発環境にcomposerをインストールします。

curl -sS https://getcomposer.org/installer | php

続いてtwitteroauthを入れます。

[vagrant@localhost bot]$ php composer.phar require abraham/twitteroauth

twitteroauthのHPに沿って、PHPを書いていきます。なお、 CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRETはtwitter developersのmy appから取得する必要があります。
https://twitteroauth.com/

<?php

require_once(__DIR__ . '/config.php');

// package
// -Composer

use Abraham\TwitterOAuth\TwitterOAuth;

$connection = new TwitterOAuth(
  CONSUMER_KEY,
  CONSUMER_SECRET,
  ACCESS_TOKEN,
  ACCESS_TOKEN_SECRET);
// $content = $connection->get("account/verify_credentials");
$content = $connection->get("statuses/home_timeline", ['count'=>3]);

var_dump($content);

crontabで指定した日時につぶやきをセットすることができます。

投票システム (1日1回)

jqueryのclick functionでopacityを変更し、更にformのvalueにidを入れて、ボタンが押されたら、MySQLにIDを格納します。そして、Mysqlからsql文でデータを読み込んで、集計結果を表示させます。また、IPアドレス、ユーザーエージェント、データで、複数投票を制限しています。

-reference
bindValue:Binds a value to a corresponding named or question mark placeholder in the SQL statement that was used to prepare the statement.
SQL INSERT INTO Statement:INSERT INTO table_name VALUES (value1,value2,value3,…);
SQLデータ取得:SELECT col_name1, col_name2, … FROM db_name.tbl_name;
SQL GROUP BY Statement:The GROUP BY statement is used in conjunction with the aggregate functions to group the result-set by one or more columns.
user agent:is software (a software agent) that is acting on behalf of a user.

index.php

<?php

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/Poll.php');

try {
  $poll = new \MyApp\Poll();
} catch (Exception $e){
  echo $e->getMessage();
  exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST'){
  $poll->post();
}

$err = $poll->getError();

?>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Poll</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<link rel="stylesheet" href="styles.css">
</head>
<body>
  <?php if (isset($err)) : ?>
  <div class="error"><?= h($err); ?></div>
<?php endif; ?>
  <h1>Which do you like best?</h1>
  <form action="" method="post">
    <div class="row">
      <div class="box" id="box_0" data-id="0"></div>
      <div class="box" id="box_1" data-id="1"></div>
      <div class="box" id="box_2" data-id="2"></div>
      <input type="hidden" id="answer" name="answer" value="">
      <input type="hidden" name="token" value="<?= h($_SESSION&#91;'token'&#93;); ?>">
    </div>
    <div id="btn">Vote and See Results</div>
  </form>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
	<script>
  $(function(){
    'use strict';

    $('.box').on('click', function(){
      $('.box').removeClass('selected');
      $(this).addClass('selected');
      $('#answer').val($(this).data('id'));
    });

    $('#btn').on('click', function(){
      if ($('#answer').val() === ''){
        alert('Choose one!');
      } else {
        $('form').submit();
      }
    });
    $('.error').fadeOut(3000);
  });
  </script>
</body>
</html>

Poll.php

<?php

namespace MyApp;

class Poll{
  private $_db;

  public function __construct(){
    $this->_connectDB();
    $this->_createToken();
  }

  private function _createToken(){
    if(!isset($_SESSION['token'])){
      $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(16));
    }
  }

  private function _validateToken(){
    if(
      !isset($_SESSION['token']) ||
      !isset($_POST['token']) ||
      $_SESSION['token'] !== $_POST['token']
    ){
      throw new \Exception('invalid token!');
    }
  }

  public function post(){
    try {
      $this->_validateToken();
      $this->_validateAnswer();
      $this->_save();
      //redirect to result.php
      header('Location: http://' . $_SERVER['HTTP_HOST'] . '/result.php');
    } catch(\Exception $e){
      // set error
      $_SESSION['err'] = $e->getMessage();
      // ridirect to index.php
      header('Location: http://' . $_SERVER['HTTP_HOST']);
    }
    exit;
  }

  public function getResults(){
    $data = array_fill(0, 3, 0);

    $sql = "select answer, count(id) as c from answers group by answer";
    foreach ($this->_db->query($sql) as $row){
      $data[$row['answer']] = (int)$row['c'];
    }
    return $data;
  }

  public function getError(){
    $err = null;
    if (isset($_SESSION['err'])){
      $err = $_SESSION['err'];
      unset($_SESSION['err']);
    }
    return $err;
  }

  private function _validateAnswer(){
    // var_dump($_POST);
    // exit;
    if (
      !isset($_POST['answer']) ||
      !in_array($_POST['answer'], [0, 1, 2])
    ){
      throw new \Exception('invalid answer!');
    }
  }

  private function _save(){
    $sql = "insert into answers
          (answer, created, remote_addr, user_agent, answer_date)
          values (:answer, now(), :remote_addr, :user_agent, now())";
    $stmt = $this->_db->prepare($sql);
    $stmt->bindValue(':answer', (int)$_POST['answer'], \PDO::PARAM_INT);
    $stmt->bindValue(':remote_addr', $_SERVER['REMOTE_ADDR'], \PDO::PARAM_STR);
    $stmt->bindValue(':user_agent', $_SERVER['HTTP_USER_AGENT'], \PDO::PARAM_STR);

    try {
      $stmt->execute();
    } catch(\PDOException $e){
      throw new \Exception('No more vote for today!');
    }
    // exit;
  }

  private function _connectDB(){
    try {
      $this->_db = new \PDO(DSN, DB_USERNAME, DB_PASSWORD);
      $this->_db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    } catch(\PDOException $e){
      throw new \Exception('Failed to connect DB');
    }
  }

}

result.php

<?php

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/Poll.php');

try {
  $poll = new \MyApp\Poll();
} catch (Exception $e){
  echo $e->getMessage();
  exit;
}

$results = $poll->getResults();
// var_dump($results);
// exit;
// $results = [
//   0 => 12,
//   1 => 32,
//   2 => 44
// ];

?>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Poll Result</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1>Result ...</h1>
    <div class="row">
      <?php for($i = 0; $i < 3; $i++) : ?>
      <div class="box" id="box_<?= h($i); ?>"><?= h($results&#91;$i&#93;); ?></div>
    <?php endfor; ?>
    </div>
    <a href="/"><div id="btn">Go Back</div></a>
</body>
</html>

styles.css

body {
  font-size: 16px;
  font-family: Arial, sans-serif;
  text-align: center;
  margin: 0;
  padding: 0;
}

h1 {
  font-size: 22px;
  margin: 30px 0;
}

.row {
  margin-bottom: 15px;
}

.box {
  width: 150px;
  height: 150px;
  cursor: pointer;
  display: inline-block;
  opacity: 0.5;
  color: #fff;
  font-size: 48px;
  font-weight: bold;
  line-height: 150px;
}
.box + .box {
  margin-left: 10px;
}

#box_0 { background: url('photo_0.jpg'); }
#box_1 { background: url('photo_1.jpg'); }
#box_2 { background: url('photo_2.jpg'); }

.selected {
  opacity: 1.0;
}

#btn {
  display: inline-block;
  width: 150px;
  padding: 7px;
  font-size: 14px;
  cursor: pointer;
  border-radius: 5px;
  background: #00aaff;
  box-shadow: 0 4px 0 #0088cc;
  color: #fff;
}

#btn:hover{
  opacity: 0.8;
}

.error{
  background: orange;
  padding: 7px;
  color: #fff;
}

mySQL

create database poll_php;
grant all on poll_php.* to dbuser@localhost identified by 'xxxx';
use poll_php
create table answers (
	id int not null auto_increment primary key,
	answer int not null,
	created datetime,
	remote_addr varchar(15),
	user_agent varchar(255),
	answer_date date,
	unique unique_answer(remote_addr, user_agent, answer_date)
);

クイズアプリ

予め正しい答えを全て配列の0に入れておき、click functionで正しいか正誤判定しています。選択肢はシャッフルして表示し、最後に、正解率を表示させています。

reference
Autoloading Classes:The spl_autoload_register() function registers any number of autoloaders, enabling for classes and interfaces to be automatically loaded if they are currently not defined. By registering autoloaders, PHP is given a last chance to load the class or interface before it fails with an error.

index.php

<?php

require_once(__DIR__ . '/config.php');

$quiz = new MyApp\Quiz();

if(!$quiz->isFinished()) {
  $data = $quiz->getCurrentQuiz();
  shuffle($data['a']);
}

?>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Intractive Art</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<link rel="stylesheet" href="styles.css">
</head>
<body>
  <?php if ($quiz->isFinished()) : ?>
    <div id="container">
      <div id="result">
        your score ...
        <div><?= h($quiz->getScore()); ?>%</div>
      </div>
     <a href=""><div id="btn">Replay</div></a>
    </div>
  <?php $quiz->reset(); ?>
  <?php else : ?>
    <div id="container">
      <h1>Q. <?= h($data&#91;'q'&#93;); ?></h1>
      <ul>
        <?php foreach ($data&#91;'a'&#93; as $a) : ?>
        <li class="answer"><?= h($a); ?></li>
        <!-- <li class="answer">A1</li>
        <li class="answer">A2</li>
        <li class="answer">A3</li> -->
        <?php endforeach; ?>
      </ul>
        <div id="btn" class="disabled"><?= $quiz->isLast() ? 'Show Result' : 'Next Question'; ?></div>
        <input type="hidden" id="token" value="<?= h($_SESSION&#91;'token'&#93;); ?>">
      </div>
  	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="quiz.js"></script>
  <?php endif; ?>
</body>
</html>

autoload.php

<?php

spl_autoload_register(function($class){
  $prefix = 'MyApp\\';

  if (strpos($class, $prefix) === 0){
    $className = substr($class, strlen($prefix));
    $classFilePath = __DIR__ . '/' . $className . '.php';

    if (file_exists($classFilePath)){
      require $classFilePath;
    } else {
        echo 'No such class' . $className;
        exit;
    }
  }

});
&#91;/php&#93;

_answer.php
&#91;php&#93;
<?php

require_once(__DIR__ . '/config.php');

$quiz = new MyApp\Quiz();

try{
  $correctAnswer = $quiz->checkAnswer();
} catch (Exception $e) {
  header($_SERVER['SERVER_PROTOCOL']. ' 403 Forbidden', true, 403);
  echo $e->getMessage();
  exit;
}

header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
  'correct_answer' => $correctAnswer
]);

Token.php

<?php

require_once(__DIR__ . '/config.php');

$quiz = new MyApp\Quiz();

try{
  $correctAnswer = $quiz->checkAnswer();
} catch (Exception $e) {
  header($_SERVER['SERVER_PROTOCOL']. ' 403 Forbidden', true, 403);
  echo $e->getMessage();
  exit;
}

header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
  'correct_answer' => $correctAnswer
]);

Quiz.php

<?php

 namespace MyApp;

 class Quiz{
   private $_quizSet = &#91;&#93;;

   public function __construct(){
     $this->_setup();
     Token::create();

     if(!isset($_SESSION['current_num'])){
       $this->_initSession();
     }
   }

   public function checkAnswer(){
     Token::validate('token');
     $correctAnswer = $this->_quizSet[$_SESSION['current_num']]['a'][0];
     if (!isset($_POST['answer'])){
       throw new \Exception('answer not set!');
     }
     if($correctAnswer === $_POST['answer']){
       $_SESSION['correct_count']++;
     }
     $_SESSION['current_num']++;
     return $correctAnswer;
   }

   public function isFinished(){
     return count($this->_quizSet) === $_SESSION['current_num'];
   }

   public function getScore() {
     return round($_SESSION['correct_count'] / count($this->_quizSet) * 100);
   }

   public function isLast(){
     return count($this->_quizSet) === $_SESSION['current_num'] + 1;
   }

   public function reset(){
     $this->_initSession();
    //  $_SESSION['current_num'] = 0;
    //  $_SESSION['correct_count'] = 0;
   }

   private function _initSession(){
     $_SESSION['current_num'] = 0;
     $_SESSION['correct_count'] = 0;
   }

   public function getCurrentQuiz() {
     return $this->_quizSet[$_SESSION['current_num']];
   }

   private function _setup(){
     $this->_quizSet[] = [
       'q' => '1543年は、ある物が日本に伝えられた年でもある。それは何か?',
       'a' => ['鉄砲', 'キリスト教', '貿易', '忍法']
     ];
     $this->_quizSet[] = [
       'q' => '海上交通の要地に作られた港町で有名なのはどこ?',
       'a' => ['堺', '伊丹', '京都', '江戸']
     ];
     $this->_quizSet[] = [
       'q' => '1639年に鎖国は完成する。この年から、来航を禁止された国はどこ?',
       'a' => ['ポルトガル', 'イギリス', '清', 'オランダ']
     ];
   }
 }

config.php

quiz.js

$(function() {
  'use strict';

  $('.answer').on('click', function() {
    var $selected = $(this);
    if ($selected.hasClass('correct') || $selected.hasClass('wrong')) {
      return;
    }
    $selected.addClass('selected');
    var answer = $selected.text();

    $.post('/_answer.php', {
      answer: answer,
      token: $('#token').val()
    }).done(function(res) {
      $('.answer').each(function() {
        if ($(this).text() === res.correct_answer) {
          $(this).addClass('correct');
        } else {
          $(this).addClass('wrong');
        }
      });
      // alert(res.correct_answer);
      if (answer === res.correct_answer) {
        // correct!
        $selected.text(answer + ' ... CORRECT!');
      } else {
        // wrong!
        $selected.text(answer + ' ... WRONG!');
      }
      $('#btn').removeClass('disabled');
    });
  });

  $('#btn').on('click', function() {
    if (!$(this).hasClass('disabled')) {
      location.reload();
    }
  });

});
body {
  font-size: 16px;
  font-family: Arial, sans-serif;
}
#container {
  width: 500px;
  margin: 15px auto;
}
h1, ul > li {
  border: 1px solid #ddd;
  border-radius: 5px;
}
h1 {
  padding: 10px;
  height: 50px;
  font-size: 18px;
  margin: 0 0 10px;
}
ul {
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
}
ul > li {
  margin-bottom: 7px;
  padding: 7px 10px;
  cursor: pointer;
}
#btn {
  text-align: center;
  float: right;
  width: 100px;
  padding: 7px;
  color: #fff;
  border-radius: 5px;
  background: #00aaff;
  box-shadow: 0 4px 0 #0088cc;
  cursor: pointer;
  font-size: 14px;
}
#btn.disabled {
  opacity: 0.5;
}
.correct{
  color: limegreen;
  font-weight: bold;
}
.wrong {
  color: #ddd;
}
.selected {
  font-weight: bold;
}
#result {
  border: 1px solid #ddd;
  border-radius: 5px;
  margin-bottom: 15px;
  text-align: center;
  padding: 30px 0 50px;
}
#result > div {
  font-size: 64px;
  font-weight: bold;
  margin-top: 30px;
}

todo app

mysqlと連携したtodoアプリです。削除に際して、チェックボックスはajaxを使っています。

reference
openssl_random_pseudo_bytes:Generate a pseudo-random string of bytes

<?php

session_start();

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/functions.php');
require_once(__DIR__ . '/Todo.php');

// get todos
$todoApp = new \MyApp\Todo();
$todos = $todoApp->getAll();

//var_dump($todos);
//exit;

// create table todos(
//   id int not null auto_increment primary key,
//   state tinyint(1) default 0,
//   title text
// );
//
// insert into todos (state, title) values
// (0, 'todo 0'),
// (0, 'todo 1'),
// (1, 'todo 2');

?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Todo app</title>
  <link rel="stylesheet" href="styles.css">
  <style>
  </style>
</head>
<body>
  <div id="container">
      <h1>Todos</h1>
      <form action="" id="new_todo_form">
        <input type="text" id="new_todo" placeholder="What needs to be done?">
      </form>
      <ul id="todos">
        <?php foreach ($todos as $todo) : ?>
          <li id="todo_<?= h($todo->id); ?>" data-id="<?= h($todo->id); ?>">
            <input type="checkbox" class="update_todo" <?php if ($todo->state === '1') { echo 'checked'; } ?>>
            <span class="todo_title <?php if ($todo->state === '1') { echo 'done'; } ?>"><?= h($todo->title); ?></span>
            <div class="delete_todo">x</div>
          </li>
        <?php endforeach; ?>
        <li id="todo_template" data-id="">
          <input type="checkbox" class="update_todo">
          <span class="todo_title"></span>
          <div class="delete_todo">x</div>
        </li>
      </ul>
  </div>
  <input type="hidden" id="token" value="<?= h($_SESSION&#91;'token'&#93;); ?>">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
  <script src="todo.js"></script>
</body>
</html>
$(function() {
  'use strict';

   $('#new_todo').focus();
  // update
  $('#todos').on('click', '.update_todo', function() {
    // idを取得
    var id = $(this).parents('li').data('id');
    // ajax処理
    $.post('_ajax.php', {
      id: id,
      mode: 'update',
      token: $('#token').val()
    }, function(res) {
      if (res.state === '1') {
        $('#todo_' + id).find('.todo_title').addClass('done');
      } else {
        $('#todo_' + id).find('.todo_title').removeClass('done');
      }
    })
  });

  // delete
  $('#todos').on('click', '.delete_todo', function() {
    // idを取得
    var id = $(this).parents('li').data('id');
    // ajax処理
    if (confirm('are you sure?')){
      $.post('_ajax.php', {
        id: id,
        mode: 'delete',
        token: $('#token').val()
      }, function() {
        $('#todo_' + id).fadeOut(800);
      });
    }
  });

  // create
  $('#new_todo_form').on('submit', function() {
    // titleを取得
    var title = $('#new_todo').val();
    // ajax処理
    $.post('_ajax.php', {
      title: title,
      mode: 'create',
      token: $('#token').val()
    }, function(res) {
      // liを追加
      var $li = $('#todo_template').clone();
      $li
        .attr('id', 'todo_' + res.id)
        .data('id', res.id)
        .find('.todo_title').text(title);
      $('#todos').prepend($li.fadeIn());
      $('#new_todo').val('').focus();
    });
    return false;
  });

});
<?php

session_start();

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/functions.php');
require_once(__DIR__ . '/Todo.php');

$todoApp = new \MyApp\Todo();

if ($_SERVER&#91;'REQUEST_METHOD'&#93; === 'POST') {
  try {
    $res = $todoApp->post();
    header('Content-Type: application/json');
    echo json_encode($res);
    exit;
  } catch (Exception $e) {
    header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);
    echo $e->getMessage();
    exit;
  }
}
<?php




namespace MyApp;

class Todo {
  private $_db;

  public function __construct() {

    $this->_createToken();

    try {
      $this->_db = new \PDO(DSN, DB_USERNAME, DB_PASSWORD);
      $this->_db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    } catch (\PDOException $e) {
      echo $e->getMessage();
      exit;
    }
  }

  private function _createToken(){
    if (!isset($_SESSION['token'])){
      $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(16));
    }
  }

  public function getAll() {
    $stmt = $this->_db->query("select * from todos order by id desc");
    return $stmt->fetchAll(\PDO::FETCH_OBJ);
  }

  public function post() {
    $this->_validateToken();
    if (!isset($_POST['mode'])) {
      throw new \Exception('mode not set!');
    }

    switch ($_POST['mode']) {
      case 'update':
        return $this->_update();
      case 'create':
        return $this->_create();
      case 'delete':
        return $this->_delete();
    }
  }

  private function _validateToken(){
    if (
      !isset($_SESSION['token']) ||
      !isset($_POST['token']) ||
      $_SESSION['token'] !== $_POST['token']
    ) {
      throw new \Exception('invalid token!');
    }
  }
  private function _update() {
    if (!isset($_POST['id'])) {
      throw new \Exception('[update] id not set!');
    }

    $this->_db->beginTransaction();

    $sql = sprintf("update todos set state = (state + 1) %% 2 where id = %d", $_POST['id']);
    $stmt = $this->_db->prepare($sql);
    $stmt->execute();

    $sql = sprintf("select state from todos where id = %d", $_POST['id']);
    $stmt = $this->_db->query($sql);
    $state = $stmt->fetchColumn();

    $this->_db->commit();

    return [
      'state' => $state
    ];

  }

  private function _create() {
    if (!isset($_POST['title']) || $_POST['title'] === '') {
      throw new \Exception('[create] title not set!');
    }

    $sql = "insert into todos (title) values (:title)";
    $stmt = $this->_db->prepare($sql);
    $stmt->execute([':title' => $_POST['title']]);

    return [
      'id' => $this->_db->lastInsertId()
    ];
  }

  private function _delete() {
    if (!isset($_POST['id'])) {
      throw new \Exception('[delete] id not set!');
    }

    $sql = sprintf("delete from todos where id = %d", $_POST['id']);
    $stmt = $this->_db->prepare($sql);
    $stmt->execute();

    return [];

  }
}

PHP カレンダー


カレンダーにはテーブルの中に、phpのdate関数フォーマットを使います。wはNumeric representation of the day of the weekです。
PHP Date:http://php.net/manual/en/function.date.php

-reference
DatePeriod class:A date period allows iteration over a set of dates and times, recurring at regular intervals, over a given period.
sprintf:Return a formatted string
var_dump: Dumps information about a variable

<?php

require 'Calendar.php';

function h($s){
  return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

$cal = new \MyApp\Calendar();

?>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Intractive Art</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="styles.css">
	<style>
	</style>
</head>
<body>
  <table>
    <thead>
        <tr>
            <th><a href="/?t=<?php echo h($cal->prev); ?>">&laquo;</a></th>
            <th colspan="5"><a href=""><?php echo h($cal->yearMonth); ?></a></th>
            <th><a href="/?t=<?php echo h($cal->next); ?>">&raquo;</a></th>
        </tr>
    </thead>
    <tbody>
      <tr>
          <td>Sun</td>
          <td>Mon</td>
          <td>Tue</td>
          <td>Wed</td>
          <td>Thu</td>
          <td>Fri</td>
          <td>Sat</td>
      </tr>
      <?php $cal->show(); ?>
    </tbody>
    <tfoot>
      <tr>
          <th colspan="7"><a href="/">Today</a></th>
      </tr>
    </tfoot>
  </table>
	<script>
	</script>
</body>
</html>
<?php

namespace MyApp;

class Calendar {
  public $prev;
  public $next;
  public $yearMonth;
  private $_thisMonth;

  public function __construct(){
    try {
      if(!isset($_GET&#91;'t'&#93;) || !preg_match('/\A\d{4}-\d{2}\z/', $_GET&#91;'t'&#93;)){
        throw new \Exception();
      }
      $this->_thisMonth = new \DateTime($_GET['t']);
    } catch (\Exception $e){
      $this->_thisMonth = new \DateTime('first day of this month');
    }
    $this->prev = $this->_createPrevLink();
    $this->next = $this->_createNextLink();
    $this->yearMonth = $this->_thisMonth->format('F Y');
  }

  private function _createPrevLink(){
    $dt = clone $this->_thisMonth;
    return $dt->modify('-1 month')->format('Y-m');
  }

  private function _createNextLink(){
    $dt = clone $this->_thisMonth;
    return $dt->modify('+1 month')->format('Y-m');
  }

  public function show() {
    $tail = $this->_getTail();
    $body = $this->_getBody();
    $head = $this->_getHead();
    $html = '<tr>' . $tail . $body . $head . '</tr>';
    echo $html;
  }
   private function _getTail(){
     $tail = '';
     $lastDayOfPrevMonth = new \DateTime('last day of ' . $this->yearMonth . ' -1 month');
     while($lastDayOfPrevMonth->format('w') < 6){
       $tail = sprintf('<td class="gray">%d</td>', $lastDayOfPrevMonth->format('d')) .
       $tail;
       $lastDayOfPrevMonth->sub(new \DateInterval('P1D'));
     }
     return $tail;
   }

   private function _getBody(){
     $body = '';
     $period = new \DatePeriod(
       new \DateTime('first day of' . $this->yearMonth),
       new \DateInterval('P1D'),
       new \DateTime('first day of ' . $this->yearMonth . ' +1 month')
     );
     $today = new \DateTime('today');
     foreach($period as $day){
       if ($day->format('w') === '0') { $body .= '</tr><tr>';}
       $todayClass = ($day->format('Y-m-d') === $today->format('Y-m-d')) ? 'today' : '';
       $body .= sprintf('<td class="youbi_%d %s">%d</td>', $day->format('w'),
       $todayClass, $day->format('d'));
     }
     return $body;
   }
   private function _getHead() {
     $head = '';
     $firstDayOfNextMonth = new \DateTime('first day of ' . $this->yearMonth . ' +1 month');
     while ($firstDayOfNextMonth->format('w') > 0) {
       $head .= sprintf('<td class="gray">%d</td>', $firstDayOfNextMonth->format('d'));
       $firstDayOfNextMonth->add(new \DateInterval('P1D'));
     }
     return $head;
   }
}






// $yearMonth = $thisMonth->format('F Y');
body {
  font-family: Arial, sans-serif;
  font-size: 14px;
}
a {
  text-decoration: none;
}
table {
  margin: 15px auto;
  border: 1px solid #ddd;
  border-collapse: collapse;
}
th {
  background: #eee;
}
th, td {
  padding: 7px;
  text-align: center;
}

.youbi_0{
  color: red;
}
.youbi_6 {
  color: blue;
}
.today {
  font-weight: bold;
}
.gray {
  color: #dedede;
}

interactive Art

addEventListener(‘click’, function(e){})で、push(new Ball(x, y, r))します。ランダム関数の範囲をとる際には、return min + Math.floor(Math.random() * (max – min + 1))と書きます。また、ボールの移動は、x++, y++, バウンドは、x軸、y軸をそれぞれ-1をかけてあげます。

reference
.getBoundingClientRect():returns the size of an element and its position relative to the viewport.
setTimeout bind: http://stackoverflow.com/questions/2130241/pass-correct-this-context-to-settimeout-callback
Object.prototype: property represents the Object prototype object.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Intractive Art</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<style>
	</style>
</head>
<body>
	<canvas id="mycanvas" width="500" height="250">
	</canvas>
	<script>
	(function(){
		'use strict';

		var canvas;
		var ctx;
		var Ball;
		var balls = [];
		var Stage;
		var stage;

		canvas = document.getElementById('mycanvas');
		if (!canvas || !canvas.getContext) return false;
		ctx = canvas.getContext('2d');

		function rand(min, max){
			return min + Math.floor(Math.random() * (max - min + 1));
		}

		function adjustPosition(pos, r, max) {
			/*if (x - r < 0) x = r;
			if (y - r < 0) y = r;
			if (x + r > canvas.width) x = canvas.width - r;
			if (y + r > canvas.height) y = canvas.height - r;*/
			if (pos - r < 0){
				return r;
			} else if (pos + r > max){
				return max - r;
			} else {
				return pos;
			}

		}

		canvas.addEventListener('click', function(e){
			var x, y, r;
			var rect;
			/*x = rand(100, 400);
			y = rand(100, 200);*/
			rect = e.target.getBoundingClientRect();
			x = e.clientX - rect.left;
			y = e.clientY - rect.top;

			r = rand(0, 100) < 20 ? rand(50, 80) : rand(10, 35);

			/*if (x - r < 0) x = r;
			if (y - r < 0) y = r;
			if (x + r > canvas.width) x = canvas.width - r;
			if (y + r > canvas.height) y = canvas.height - r;*/
			x = adjustPosition(x, r, canvas.width);
			y = adjustPosition(y, r, canvas.height);
			balls.push(new Ball(x, y, r));
		});

		Ball = function(x, y, r){
			this.x = x;
			this.y = y;
			this.r = r;
			this.vx = rand(-10, 10);
			this.vy = rand(-10, 10);
			this.color = 'hsla(' + rand(50, 100)+ ', ' + rand(40, 80) + '%, ' + rand(50, 60) +'%, ' + Math.random() + ')';
		};

		/*var ball = new Ball(rand(100, 200), rand(100, 200), rand(10, 50));
		ball.draw();*/

		Ball.prototype.draw = function(){
			ctx.beginPath();
				ctx.arc(this.x, this.y, this.r, 0, Math.PI*2);
				ctx.fillStyle = this.color;
				ctx.closePath();
				ctx.fill();

		};

		Ball.prototype.move = function(){
			if (this.x + this.r > canvas.width || this.x - this.r < 0){
				this.vx *= -1;
			}
			if (this.y + this.r > canvas.height || this.y - this.r < 0){
				this.vy *= -1;
			}
			this.x += this.vx;
			this.y += this.vy;

		};

		Stage = function(){
			this.update = function(){
					var i;
					this.clear();
			
			for (i = 0; i < balls.length; i++){
				balls&#91;i&#93;.draw();
				balls&#91;i&#93;.move();
			}
			setTimeout(function(){
				this.update();
			}.bind(this), 30);

			};
			this.clear = function(){
				ctx.fillStyle = "#ecf0f1";
				ctx.fillRect(0, 0, canvas.width, canvas.height);
			}
		};

		stage = new Stage();
		stage.update();

	})();
	</script>
</body>
</html>

15puzzle

タイルを動かして、1-15の順番に並べるゲームで、各数字に割り当てるx座標y座標をMathfloorのwidth,heightで割って、配列しています。var moveCountの数字を動かすことによって、ゲームの難易度を変更します。

reference
clientX Property:returns the horizontal coordinate (according to the client area) of the mouse pointer when a mouse event was triggered.The client area is the current window.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>15puzzle</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<style>
	</style>
</head>
<body>
	<canvas id="stage" width="280" height="280">
		Canvas not supported ...
	</canvas>
	<script>
		(function(){
			'use strict';

			var canvas = document.getElementById('stage');
			var context;
			var image;
			var IMAGE_URL = '15puzzle.png';
			var tiles = [];
			var ROW_COUNT = 4;
			var COL_COUNT = 4;
			var PIC_WIDTH = 280;
			var PIC_HEIGHT = 280;
			var TILE_WIDTH = PIC_WIDTH / COL_COUNT;
			var TILE_HEIGHT = PIC_HEIGHT / ROW_COUNT;
			var UDLR = [
				[ 0, -1],
				[ 0, 1],
				[ -1, 0],
				[ 1, 0],
			];
			var moveCount = 3;

			function initTiles(){
				var row, col;
				for (row = 0; row < ROW_COUNT; row++){
					tiles&#91;row&#93; = &#91;&#93;;
					for (col = 0; col < COL_COUNT; col++){
						tiles&#91;row&#93;&#91;col&#93; = row * COL_COUNT + col;
					}
				}
				tiles&#91;ROW_COUNT - 1&#93;&#91;COL_COUNT - 1&#93; = -1;
			}

			function drawPuzzle(){
				var row, col;
				var sx, sy;
				var dx, dy;

				for (row = 0; row < ROW_COUNT; row++){
					for (col = 0; col < COL_COUNT; col++){
						dx = col * TILE_WIDTH;
						dy = row * TILE_HEIGHT;
						if (tiles&#91;row&#93;&#91;col&#93; === -1){
							context.fillStyle = '#eeeeee';
							context.fillRect(dx, dy, TILE_WIDTH, TILE_HEIGHT);
						}else {
						sx = (tiles&#91;row&#93;&#91;col&#93; % COL_COUNT) * TILE_WIDTH;
						sy = Math.floor((tiles&#91;row&#93;&#91;col&#93; / ROW_COUNT)) * TILE_HEIGHT;
				
						context.drawImage(image, sx, sy, TILE_WIDTH, TILE_HEIGHT, dx, dy, TILE_WIDTH, TILE_HEIGHT);
						}
						
					}
				}
	
				/*context.drawImage(image, 0, 0);*/
			}

			function checkResult(){
				var row, col;
				for (row = 0; row < ROW_COUNT; row++){
					for (col = 0; col < COL_COUNT; col++){
						if (row === ROW_COUNT -1 && col === COL_COUNT -1){
							return true;
						}
						if (tiles&#91;row&#93;&#91;col&#93; !== row * COL_COUNT + col){
							return false;
						}
				}
			}

			}
			function moveBlank(count){
				var blankRow, blankCol;
				var targetPosition;
				var targetRow, targetCol;

				blankRow = ROW_COUNT -1;
				blankCol = COL_COUNT -1;

				while(true){
				targetPosition = Math.floor(Math.random() * UDLR.length);
				targetRow = blankRow + UDLR&#91;targetPosition&#93;&#91;1&#93;;
				targetCol = blankCol + UDLR&#91;targetPosition&#93;&#91;0&#93;;
				if(targetRow < 0 || targetRow >= ROW_COUNT){
					continue;
				}
				if(targetCol < 0 || targetCol >= COL_COUNT){
					continue;
				}
				tiles[blankRow][blankCol] = tiles[targetRow][targetCol];
				tiles[targetRow][targetCol] = -1;
				blankRow = targetRow;
				blankCol = targetCol;
				if(!--count){
					break;
				}
				} 
			}

			if (!canvas.getContext){
				alert('canvas not supported ...');
				return
			}
			context = canvas.getContext('2d');

			image = document.createElement('img');
			image.src = IMAGE_URL;
			image.addEventListener('load', function(){
				initTiles();
				moveBlank(moveCount);
				drawPuzzle();
			});

			canvas.addEventListener('click', function(e){
				var x, y;
				var rect;
				var row, col;
				var i;
				var targetRow, targetCol;
				rect = e.target.getBoundingClientRect();
				x = e.clientX - rect.left;
				y = e.clientY - rect.top;
				row = Math.floor(y / TILE_HEIGHT);
				col = Math.floor(x / TILE_WIDTH);
				if (tiles[row][col] === -1){
					return;
				}
				 for (i = 0; i < UDLR.length; i++) {
       			 targetRow = row + UDLR&#91;i&#93;&#91;1&#93;;
      			  targetCol = col + UDLR&#91;i&#93;&#91;0&#93;;
      			  if (targetRow < 0 || targetRow >= ROW_COUNT) {
      		  	 	 continue;
     			   }
     			  if (targetCol < 0 || targetCol >= COL_COUNT) {
     		     	 continue;
    			    }
					if (tiles[targetRow][targetCol] === -1) {
         			 tiles[targetRow][targetCol] = tiles[row][col];
         			 tiles[row][col] = -1;
       				   drawPuzzle();
       				   if (checkResult()){
       				   	alert('Game Clear!')
       				   }
						break;
					}
				}
			});
		})();
	</script>
</body>
</html>

神経衰弱

レベルに合わせて、配列を作成し、CSSでflipし、judgeして、正しければ次のレベルに移行します。

-reference
Emmet Toggle Comment: ctr + shift + /
CSS3 backface-visibility Property: The backface-visibility property defines whether or not an element should be visible when not facing the screen.This property is useful when an element is rotated, and you do not want to see its backside.
first-child:is used to select the specified selector, only if it is the first child of its parent.
.textContent:ノードとその子孫のテキスト内容を取得、または設定
.children:Get the children of each element in the set of matched elements, optionally filtered by a selector.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Get same number</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<style>
		body {
			margin: 0;
			background: #00d4f0;
			text-align: center;
			font-family: Arial, sans-serif;
			color: #fff;
		}
		#stage{
			margin: 80px auto 30px;
			width: 600px;
		}
		.card-container {
			margin: 0 10px 20px 0;
			cursor: pointer;
			display: inline-block;
			width: 50px;
			height: 60px;
			font-size: 24px;
			font-weight: bold;
			line-height: 60px;
			position: relative;
			perspective: 100px;
		}
		.card {
			width: 100%;
			height: 100%;
			position: absolute;
			transform-style: preserve-3d;
			transition: transform .8s;
		}
		.card.open {
			transform: rotateY(180deg);
		}
		.card-back, .card-front {
			display: block;
			width: 100%;
			height: 100%;
			border-radius: 5px;
			position: absolute;
			backface-visibility: hidden;
		}
		.card-back{
			background: #03a9f4;
		}
		.card-front{
			background: #fff;
			color: #333;
			transform: rotateY(180deg);
		}
		#btn {
			margin: 0 auto;
			width: 200px;
			padding: 10px;
			background: #0af;
			border-radius: 5px;
			cursor: pointer;
			opacity: 0;
			transition: opacity .5s;
		}
		#btn.visible{
			opacity: 1;
		}
	</style>
</head>
<body>
	<div id="stage">
	</div>
	<div id="btn">Next Stage?</div>
	<script>
	(function(){
		var cards = [],
		    level = 2,
		    flipCount = 0,
		    correctCount = 0,
		    firstCard = null,
		    secondCard = null;
		    btn,
		    stage;

		  btn = document.getElementById('btn');
		  stage = document.getElementById('stage');

		init();

		function init(){

			correctCount = 0;
			btn.className = '';

			while(stage.firstChild) stage.removeChild(stage.firstChild);

			for (var i = 1; i <= level; i++){
				cards&#91;cards.length&#93; = createCard(i);
				cards&#91;cards.length&#93; = createCard(i);
			}

			while (cards.length){
				var pos = Math.floor(Math.random() * cards.length);
				stage.appendChild(cards.splice(pos, 1)&#91;0&#93;);
			}
		}

		function createCard(num){
			var inner,
				card,
				container;

			inner = '<div class="card-back">?</div><div class="card-front">*</div>';

			card = document.createElement('div');
			card.className = 'card';
			card.innerHTML = inner.replace('*', num);

			container = document.createElement('div');
			container.className = 'card-container';
			container.appendChild(card);
			card.addEventListener('click', function(){
				flipCard(this);
			});

			return container;
		}

		function flipCard(card){
			if (firstCard !== null && secondCard !== null){
				return;
			}

			if (card.className.indexOf('open') === -1){
				card.className = 'card open';
			} else {
				return;
			}
			flipCount++;
			if (flipCount % 2 === 1) {
				firstCard = card;
			} else {
				secondCard = card;
				setTimeout(function(){
					judge();
				}, 900);
			}
		}

		function judge(){
			if (firstCard.children[1].textContent === secondCard.children[1].textContent){
				correctCount++;
				//console.log(correctCount);
				if (correctCount === level){
					btn.className = 'visible';
				}
			} else {
				firstCard.className = 'card';
				secondCard.className = 'card';
			}
			firstCard = null;
			secondCard = null;
		}

		btn.addEventListener('click', function(){
			level++;
			init();
		});
	})();
	</script>
</body>
</html>

数押しゲーム

addEventListener(‘click’, function(){}で、クリックごとに、btn inactiveのクラスを追加していってます。数字の初期値はランダム、時間はsetTimeoutで、現在時間からスタート時間を引いています。

reference
appendChild(): method appends a node as the last child of a node.
Tip: If you want create a new paragraph, with text, remember to create the text as a Text node which you append to the paragraph, then append the paragraph to the document.
Document.createElement() :method creates the HTML element specified by tagName, or an HTMLUnknownElement if tagName isn’t recognized. In a XUL document, it creates the specified XUL element. In other documents, it creates an element with a null namespace URI.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Run Time for Button touch</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<style>
		body {
			background: #e0e0e0;
			font-family: Arial, sans-serif;
			text-align: center;
			color: #0088cc;
			font-size: 16px;
		}
		#scoreArea{
			text-shadow: 0 1px 0 rgba(255, 255,255, 0.5);
			margin: 30px auto;
		}
		#scoreText{
			font-weight: bold;
			font-size: 24px;
		}
		#startButton{
			margin: 30px auto;
			width: 180px;
			border-radius: 5px;
			box-shadow: 0 4px 0 #e91b0c;
			background: #f44336;
			color: #fff;
			cursor: pointer;
			padding: 7px;
		}
		#startButton:hover{
			opacity: 0.8;
		}
		#startButton.pushed{
			margin-top: 32px;
			box-shadow: 0 2px 0 #e91b0c;
		}
		.btn {
			position: relative;
			display: inline-block;
			width: 30px;
			padding: 7px;
			border-radius: 5px;
			box-shadow: 0 4px 0 #0088cc;
			color: #fff;
			background: #00aaff;
			cursor: pointer;
			margin-bottom: 10px;
		}
		.btn.hidden:after{
			content: '?';
			position: absolute;
			top: 0;
			left: 0;
			width: 30px;
			padding: 7px;
			border-radius: 5px;
			background: #00aaff;
		}
		.btn + .btn {
			margin-left: 10px;
		}
		.btn.inactive {
			opacity: 0.5;
		}
	</style>
</head>
<body>
	<div id="scoreArea">Your Score: <span id="scoreText">0.0</span></div>
	<div id="board">
		<!--<div class="btn">0</div>
		<div class="btn">1</div><br>
		<div class="btn">2</div>
		<div class="btn">3</div> -->
	</div>
	<div id="startButton" class="pushed">START</div>
	<script>
	(function(){
		'use strict';
		var size = 3;
		var currentNum = 0;
		var startTime;
		var timerId;

		var board = document.getElementById('board');
		var scoreText = document.getElementById('scoreText');
		var startButton = document.getElementById('startButton');

		initBoard();

		startButton.addEventListener('click', function(){
			initBoard();
			var btns = document.getElementsByClassName('btn');
			for (var i = 0; i < btns.length; i++){
				btns&#91;i&#93;.className = 'btn';
			}
			currentNum = 0;
			startTime = Date.now();
			if(timerId !== null) clearTimeout(timerId);
			runTimer();
		});

		startButton.addEventListener('mousedown', function(){
			this.className = 'pushed';
		});

		startButton.addEventListener('mouseup', function(){
			this.className = '';
		});


		function runTimer(){
			scoreText.innerHTML = ((Date.now() - startTime) / 1000).toFixed(2);
			timerId = setTimeout(function(){
				runTimer();
			}, 10);
		}

			function createButton(num){
				var button;
				button = document.createElement('div');
				button.className = 'btn hidden';
				button.innerHTML = num;
				button.addEventListener('click', function(){
					if ((this.innerHTML - 0) === currentNum){
						this.className = 'btn inactive';
						currentNum++;
					}
					if (currentNum == size * size){
						clearTimeout(timerId);
					}
				});
				return button;
			}

			function initBoard(){
			var buttons = &#91;&#93;;

			while (board.firstChild){
				board.removeChild(board.firstChild);
			}

			for (var i = 0; i < size * size; i ++){
				buttons.push(createButton(i));
			}
			while (buttons.length){
				var button = buttons.splice(Math.floor(Math.random() * buttons.length), 1);
				board.appendChild(button&#91;0&#93;);
				if (buttons.length % size === 0){
					board.appendChild(document.createElement('br'));
				}
			}
		}
	})();
	</script>
</body>
</html>

タイピングゲーム

fromCharCodeとkeyCodeで、タイピングのcharacterを取得し、予め用意された配列とマッチするか判定しています。マッチしていれば、位置文字ずつ”_”に文字を変換し、残りの文字をsubstring()で取得します。タイマーは、setTimeout()で1000ミリ秒ごとに減らしていき、0になったら、alert()を表示させます。

リフェレンス
fromCharCode() Method:converts Unicode values into characters.
Note: This is a static method of the String object, and the syntax is always String.fromCharCode().
substring():extracts the characters from a string, between two specified indices, and returns the new sub string.
setTimeout():calls a function or evaluates an expression after a specified number of milliseconds.
Tip: 1000 ms = 1 second.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>タイピングゲーム</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<style>
	body {
		padding-top: 40;
		font-family: 'Courier New', sans-serif;
		text-align: center;
	}
	#target {
		font-size: 48px;
		letter-spacing: 3px;
	}
	.info {
		color: #ccc;
	}
	</style>
</head>
<body>
	<p id="target"></p>
	<p class="info">
		Letters count: <span id="score"></span>
		Miss count: <span id="miss"></span>
		Remaining Time: <span id="timer"></span>
	</p>
	<script>
	(function(){
		'use strict';

		var words = [
		'java',
		'python',
		'ruby',
		'go',
		'c',
		'javascript',
		'cobol',
		'php',
		];
		var currentWord;
		var currentLocation;
		var score;
		var miss;
		var timer;
		var target = document.getElementById('target');
		var scoreLabel = document.getElementById('score');
		var missLabel = document.getElementById('miss');
		var timerLabel = document.getElementById('timer');
		var isStarted;
		var timerId;

		function init(){
			currentWord = 'click to start';
			currentLocation = 0;
			score = 0;
			miss = 0;
			timer = 7;
			target.innerHTML = currentWord;
			scoreLabel.innerHTML = score;
			missLabel.innerHTML = miss;
			timerLabel.innerHTML = timer;
			isStarted = false;
		}
		
		init();

		function updateTimer(){
			timerId = setTimeout(function(){
				timer--;
				timerLabel.innerHTML = timer;
				if (timer <= 0){
					// alert('game over');
					var accuracy = (score + miss) === 0 ? '0.00' : (score / (score + miss) * 100).toFixed(2);
					alert(score + ' letters, ' + miss + ' miss!' +  accuracy + ' % accuracy');
					clearTimeout(TimerId);
					init();
					return;
				}
				updateTimer();
			}, 1000);
		}

		function setTarget(){
			currentWord = words&#91;Math.floor(Math.random()* words.length)&#93;;
			target.innerHTML = currentWord;
			currentLocation = 0;
		}

		window.addEventListener('click', function() {
			if (!isStarted){
				isStarted = true;
				setTarget();
				updateTimer();
			}
			
		});

		window.addEventListener('keyup', function(e) {
			if (!isStarted){
				return;
			}
			if (String.fromCharCode(e.keyCode) ===
				currentWord&#91;currentLocation&#93;.toUpperCase()){
				currentLocation++;
				var placeholder = '';
				for (var i = 0; i < currentLocation; i++){
					placeholder += '_';
				}
				target.innerHTML = placeholder + currentWord.substring(currentLocation);
				score++;
				scoreLabel.innerHTML = score;
				if (currentLocation === currentWord.length){
					setTarget();
				}
			} else {
				miss++;
				missLabel.innerHTML = miss;
			}
		});
	})();
	</script>
</body>
</html>