みかづきブログ その3

本ブログは更新を終了しました。通算140万ユーザーの方に観覧頂くことができました。長い間、ありがとうございました。

👆

引越し先はこちらです!

IEでもCanvasに紙吹雪を舞い散らせてみました。

kimizuka.hatenablog.com

昔書いたコードをIE用にトランスコンパイルしてみました。
これで、IE10、IE11でも動くようになったと思います。

DEMO


JavaScript

"use strict";

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Progress = function () {
  function Progress() {
    var param = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];

    _classCallCheck(this, Progress);

    this.timestamp = null;
    this.duration = param.duration || Progress.CONST.DURATION;
    this.progress = 0;
    this.delta = 0;
    this.progress = 0;
    this.isLoop = !!param.isLoop;

    this.reset();
  }

  Progress.prototype.reset = function reset() {
    this.timestamp = null;
  };

  Progress.prototype.start = function start(now) {
    this.timestamp = now;
  };

  Progress.prototype.tick = function tick(now) {
    if (this.timestamp) {
      this.delta = now - this.timestamp;
      this.progress = Math.min(this.delta / this.duration, 1);

      if (this.progress >= 1 && this.isLoop) {
        this.start(now);
      }

      return this.progress;
    } else {
      return 0;
    }
  };

  _createClass(Progress, null, [{
    key: "CONST",
    get: function get() {
      return {
        DURATION: 1000
      };
    }
  }]);

  return Progress;
}();

var Confetti = function () {
  function Confetti(param) {
    _classCallCheck(this, Confetti);

    this.parent = param.elm || document.body;
    this.canvas = document.createElement("canvas");
    this.ctx = this.canvas.getContext("2d");
    this.width = param.width || this.parent.offsetWidth;
    this.height = param.height || this.parent.offsetHeight;
    this.length = param.length || Confetti.CONST.PAPER_LENGTH;
    this.yRange = param.yRange || this.height * 2;
    this.progress = new Progress({
      duration: param.duration,
      isLoop: true
    });
    this.rotationRange = typeof param.rotationLength === "number" ? param.rotationRange : 10;
    this.speedRange = typeof param.speedRange === "number" ? param.speedRange : 10;
    this.sprites = [];

    this.canvas.style.cssText = ["display: block", "position: absolute", "top: 0", "left: 0", "pointer-events: none"].join(";");

    this.render = this.render.bind(this);

    this.build();

    this.parent.appendChild(this.canvas);
    this.progress.start(performance.now());

    requestAnimationFrame(this.render);
  }

  Confetti.prototype.build = function build() {
    for (var i = 0; i < this.length; ++i) {
      var canvas = document.createElement("canvas"),
          ctx = canvas.getContext("2d");

      canvas.width = Confetti.CONST.SPRITE_WIDTH;
      canvas.height = Confetti.CONST.SPRITE_HEIGHT;

      canvas.position = {
        initX: Math.random() * this.width,
        initY: -canvas.height - Math.random() * this.yRange
      };

      canvas.rotation = this.rotationRange / 2 - Math.random() * this.rotationRange;
      canvas.speed = this.speedRange / 2 + Math.random() * (this.speedRange / 2);

      ctx.save();
      ctx.fillStyle = Confetti.CONST.COLORS[Math.random() * Confetti.CONST.COLORS.length | 0];
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.restore();

      this.sprites.push(canvas);
    }
  };

  Confetti.prototype.render = function render(now) {
    var progress = this.progress.tick(now);

    this.canvas.width = this.width;
    this.canvas.height = this.height;

    for (var i = 0; i < this.length; ++i) {
      this.ctx.save();
      this.ctx.translate(this.sprites[i].position.initX + this.sprites[i].rotation * Confetti.CONST.ROTATION_RATE * progress, this.sprites[i].position.initY + progress * (this.height + this.yRange));
      this.ctx.rotate(this.sprites[i].rotation);
      this.ctx.drawImage(this.sprites[i], -Confetti.CONST.SPRITE_WIDTH * Math.abs(Math.sin(progress * Math.PI * 2 * this.sprites[i].speed)) / 2, -Confetti.CONST.SPRITE_HEIGHT / 2, Confetti.CONST.SPRITE_WIDTH * Math.abs(Math.sin(progress * Math.PI * 2 * this.sprites[i].speed)), Confetti.CONST.SPRITE_HEIGHT);
      this.ctx.restore();
    }

    requestAnimationFrame(this.render);
  };

  _createClass(Confetti, null, [{
    key: "CONST",
    get: function get() {
      return {
        SPRITE_WIDTH: 9,
        SPRITE_HEIGHT: 16,
        PAPER_LENGTH: 100,
        DURATION: 8000,
        ROTATION_RATE: 50,
        COLORS: ["#EF5350", "#EC407A", "#AB47BC", "#7E57C2", "#5C6BC0", "#42A5F5", "#29B6F6", "#26C6DA", "#26A69A", "#66BB6A", "#9CCC65", "#D4E157", "#FFEE58", "#FFCA28", "#FFA726", "#FF7043", "#8D6E63", "#BDBDBD", "#78909C"]
      };
    }
  }]);

  return Confetti;
}();

(function () {
  var DURATION = 8000,
      LENGTH = 120;

  new Confetti({
    width: window.innerWidth,
    height: window.innerHeight,
    length: LENGTH,
    duration: DURATION
  });

  setTimeout(function () {
    new Confetti({
      width: window.innerWidth,
      height: window.innerHeight,
      length: LENGTH,
      duration: DURATION
    });
  }, DURATION / 2);
})();

iOS11のSafariからカメラとマイクにアクセスするシンプルでサンプルなコードを書きました。

iOS11がリリース(9月20日)されてから、はや1ヶ月半弱、
iPhone8が発売(9月22日)されてからも、はや1ヶ月半弱、
iPhoneXが発売(11月3日)されてしてから、はや3日が立ち、

32ビットアプリが動かなくなるという情報からか、リリース直後は若干苦戦していたiOS11のシェアも、
アナリティクスを確認する限りでは、着々と増えてきております。


さてさて、

New in Safari 11.0 – Camera and microphone access.
 Added support for the Media Capture API.
 Websites can access camera and microphone streams from a user's device (user permission is required.)

https://developer.apple.com/library/content/releasenotes/General/WhatsNewInSafari/Safari_11_0/Safari_11_0.html より引用

とあるように、iOS11からはウェブサイトからカメラマイクにアクセスできるようになりました。
iOS11のシェアが増えてきたいまこそ、ウェブサイトからカメラマイクにアクセスするサイトをつくるチャンスなのではないかと思い、
シンプルなサンプルを書いてみました。ソースはリポジトリにアップしております。

カメラにアクセス
github.com

マイクにアクセス
github.com





はじめに

  1. マイク、カメラにアクセスするためにはhttpsのサイトであることが必須条件です。
  2. Androidでは動作確認しておりません。



カメラにアクセス編

フロントカメラにアクセス

f:id:kimizuka:20171106161950p:plain

https://kimizuka.github.io/webcamera-preview-for-ios11/front-camera/

明示的にフロントカメラを渡していますが、

medias = {audio : false, video : true}

という感じで、明示的に渡さなくても基本的にフロントカメラが立ち上がります。

ソースコード

JavaScript

const medias = {
  audio: false,
  video: {
    facingMode: "user" // フロントカメラにアクセス
  }
};
const video = document.getElementById("video");
const promise = navigator.mediaDevices.getUserMedia(medias);

promise.then(successCallback)
       .catch(errorCallback);


function successCallback(stream) {
  video.srcObject = stream;
};

function errorCallback(err) {
  alert(err);
};

HTML

<video id="video" autoplay playsinline></video><!--インライン再生を可能にしておく-->

CSS

body {
  margin: 0;
  background: #000;
}

#video {
  display: block;
  width: 100%;



リアカメラにアクセス

f:id:kimizuka:20171106162015p:plain

https://kimizuka.github.io/webcamera-preview-for-ios11/rear-camera/

ソースコード

JavaScript

const medias = {
  audio: false,
  video: {
    facingMode: {
      exact: "environment" // リアカメラにアクセス
    }
  }
};
const video = document.getElementById("video");
const promise = navigator.mediaDevices.getUserMedia(medias);

promise.then(successCallback)
       .catch(errorCallback);

function successCallback(stream) {
  video.srcObject = stream;
};

function errorCallback(err) {
  alert(err);
};

HTML

<video id="video" autoplay playsinline></video><!--インライン再生を可能にしておく-->

CSS

body {
  margin: 0;
  background: #000;
}

#video {
  display: block;
  width: 100%;



応用例

f:id:kimizuka:20171106162042p:plain

https://kimizuka.github.io/webcamera-preview-for-ios11/carnvas-demo/

f:id:kimizuka:20171106133159g:plain

車窓とあわせる用の簡易ARのデモです。
すべてcanvasにレンダリングしても良かったのですが、カメラから取り込んだ映像以外はCSSで描画しています。
画面をタップするとジャンプします。
折角CSSを使っているので、targetセレクタをつかって、#reverseで反転させようと思ったのですが、
何故かiOSでうまくいきません。(原因調査中)
ソースはリポジトリを直接見ていただいたほうが早いと思います。



マイクにアクセス編

マイク入力をビジュアライズ

f:id:kimizuka:20171106162110p:plain

https://kimizuka.github.io/mic-preview-for-ios11/analyse/

f:id:kimizuka:20171106135846g:plain

iOSの制約で「ユーザージェスチャーきっかけで無いとオーディオを再生できない」という点に苦戦しました、
はじめに画面をタップさせることで制約を回避しました。
FFTを掛けて画面上にビジュアライスしています。

ソースコード

JavaScript

(function() {

  "use strict";

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

  navigator.mediaDevices.getUserMedia({
    audio: true,
    video: false
  }).then(_handleSuccess).catch(_handleError);
  
  function _handleSuccess(stream) {
    btn.addEventListener("click", () => {
      _handleClick(stream);
    }, false);
  }

  function _handleError() {
    alert("Error!");
  }

  function _handleClick(stream) {
    const LENGTH = 16;
    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    const options  = {
      mediaStream : stream
    };
    const src = audioCtx.createMediaStreamSource(stream);
    const analyser = audioCtx.createAnalyser(stream);
    const data = new Uint8Array(LENGTH);
    let w = 0;

    btn.classList.add("off");
    analyser.fftSize = 1024;
    src.connect(analyser);

    setInterval(() => {
      canvas.width  = window.innerWidth;
      canvas.height = window.innerHeight;

      ctx.fillStyle = "#3e3e3e";

      w = canvas.width / LENGTH,

      analyser.getByteFrequencyData(data);

      for (let i = 0; i < LENGTH; ++i) {
        ctx.rect(i * w, canvas.height - data[i] * 2, w, data[i] * 2);
      }

      ctx.fill();
    }, 20);
  }
})();

HTML

<canvas id="canvas"></canvas>
<div id="btn">TAP TO START</div>

CSS

body {
  font: 20px "Rubik Mono One", sans-serif;;
  background: #e3e3e3;
  overflow: hidden;
}

#btn {
  display: flex;
  position: absolute;
  top: 0; bottom: 0;
  left: 0; right: 0;
  color: #3e3e3e;
  align-items: center;
  justify-content: center;
  opacity: 1;
  transition: opacity .2s ease-in-out;
}

#btn.off {
  opacity: 0;
  pointer-events: none;
}

#canvas {
  display: block;
}



音声認識(失敗)

f:id:kimizuka:20171106162152p:plain

こちらでつくったサンプルをiOS11で試してみましたが、

kimizuka.hatenablog.com

残念ながら、動作しませんでした。
それもそのはず、SpeechRecognitionはまだ未実装ですね。

developer.mozilla.org


今回はiOS11に限ったコードを書きましたが、
実践に投入するにはAndroidやPCでも動くようにしなければならないですね。
それは、そのうち頑張って書こうと思います。今回は以上となります。



追記: 2018/02/17
Navigator.getUserMediaが非推奨となったため、MediaDevices.getUserMediaを使うように修正しました。

横浜駅周辺で激辛料理が食べたくなった時の為のまとめ 2017

kimizuka.hatenablog.com

こんな記事 を書くぐらいには辛い物好きな私としましては、
横浜に「 蒙古タンメン中本 」がやってきたのが嬉しくてしょうが無いわけです。

www.moukotanmen-nakamoto.com

ここで3年ぶりに、横浜辛いものまとめを更新しておきたいと思います。
今回は若干関内方面もはいってます。



奴が愛したタンタンメン メーギ・テントーチ

奴が愛したタンタンメン メーギ・テントーチ - Google 検索


いきなり関内方面のお店です。
まず、世の中には「担々麺」と「タンタンメン」があります。
「担々麺」とは、胡麻の風味とラー油の辛さが特徴のラーメン。
「たんたんめん」という音を聞いたときに多くの人はこちらを想像すると思います。

個人的には「担々麺」といったら「四川翔」の「担々麺」がいちばん好きです。
大垣(岐阜)の店ですが。

四川翔 - Google 検索

一方、「タンタンメン」とは、にんにく、ラー油の辛さと溶き卵の甘さが特徴のラーメン。
元祖ニュータンタンメン本舗」が有名です。

newtantan.com

そう。一口に「たんたんめん」といっても、
同音異義語で2種類のラーメンがある。僕はそう思っておりました。

で、「奴が愛したタンタンメン メーギ・テントーチ」はどちらの「たんたんめん」なのかといいますと、
文字のごとく「タンタンメン」に近いのですが、また全然違ったラーメンに仕上がっているのです。
ラーメンというか、あんかけラーメンのようなドロドロさなんですね。
辛さも申し分なく、個人的にはとても好きなお店です。
個人的おすすめメニューは「えび&チーズタンタンメンの辛さ4」です。



べいらっきょ

Soup Curry べいらっきょ

つぎも関内方面のお店です。
辛いスープカレーといえば、 マジックスパイス が有名ですが、

www.magicspice.net

個人的には、辛さの中に甘みがほしいので、 イエローカンパニー「豚角煮の辛さ圏外」がとても好きだったんです。
都内に住んでいる時は毎週通っていました。

www.yellowcompany.jp

しかし、いまは横浜住まい。
横浜でイエローカンパニーっぽいスープカレーが食べたかったら、
迷わずべいらっきょだと思います。

個人的おすすめメニューは「広島産カキカレーのレベル6」です。



元祖カレータンタン麺 征虎 総本店

元祖カレータンタン麺 征虎 総本店 - Google 検索

横浜駅でもなければ関内駅でもなくなりました。
最寄り駅は黄金町です。

まず、世の中には「担々麺」と「タンタンメン」がありまして、
デジャビュなんで違いの説明は省略しますね。

で、こちらの「たんたんめん」は「タンタンメン」です。
カレーがかかってます。

ただ、個人的にカレー×麺に関しては、
「麺屋 波 wave」の「カレーつけ麺の敦盛、激辛、麺少なめ」が最強だと思っているので。
鎌倉の店なんですけどね。

麺屋 波 wave - Google 検索

カレータンタン麺を頂きました。レベル7でも大丈夫でした。



蒙古タンメン中本 横浜

www.moukotanmen-nakamoto.com

11月1日にオープンしてから何回か見に行っているのですが、
まだまだ行列がすごく、諦めて帰ってくる日々でした。
40分ぐらい並んだら入ることができたのですが、
そこでわかったことは、行列は店の外だけではないということ。
店内にも10人ぐらい並んでいました。
個人的には冷やし味噌ラーメン一択です。



鶴一家

www.dandan.cc


ようやく横浜駅周辺のお店です。
家系ラーメンを順当に辛くした感じのラーメンです。
「地獄ラーメンの激辛」を頂きました。辛さ的には余裕でした。



横濱ハイハイ樓 横浜西口店

鶴一家 - Google 検索

こちらも横浜西口のお店です。
メニューに「ハイカラ麺」という。ピリ辛のラーメンはあるものの、実はラーメン自体はあんまり辛くありません。
ただ、テーブルの上に生唐辛子がおいてありまして、それをがんがん入れることでそれなりの辛さのラーメンを生成することができるのです。



大勝軒 横浜西口店

www.tai-sho-ken.com

横濱ハイハイ樓のほぼ正面にあるお店です。
「激辛そば」を頂きました。
辛辛魚 + 台湾ラーメン のような感じでした。
横濱ハイハイ樓の「ハイカラ麺」よりは辛いですが、まだまだ余裕の辛さでした。



今回は以上です。
ちょっと遠いんですが、
「奴が愛したタンタンメン メーギ・テントーチ」「えび&チーズタンタンメンの辛さ4」と、
「べいらっきょ」「広島産カキカレーのレベル6」はとてもおすすめですよ。

横浜駅周辺ですませたい時は、しばらくは「蒙古タンメン中本」「冷やし味噌ラーメン」を食べようと思います。