大阪万博ロゴ いのちの輝きくんブームに便乗してみました――ソースコード

ここで公開している画像とか、目の座標とか、JavaScriptのソースについて、私は一切著作権を主張しませんので、ご自由にご利用ください。画像については2025年日本国際博覧会協会に権利があるのでしょうが、私がここで公開しているプログラムごときで怒られることはないと思っています。ただし、ここの画像を2次使用して怒られたとしても、私は一切の責任を負いません。

基本的な手順

まずは原形となるロゴを取得しなければ話にならないわけですが、いろいろなページにいろいろな大きさのロゴが貼られている中、私は、2025年日本国際博覧会協会【公式】のTwitter投稿から画像を取得しました。

そこから瞳を消す必要がありますが、私はPhotoshopとか高級なツールは持ってないし使い方も知らないので、ペイントでがんばって消したのがこちら。

そこから、それぞれの目の楕円を囲む長方形を、JTrimでちまちまと目視で確認した結果が、JavaScriptのソースに入っているこの配列です。

const eyes = [
  {xMin: 237, yMin:  18, xMax: 286, yMax:  66},
  {xMin: 267, yMin: 255, xMax: 326, yMax: 309},
  {xMin: 176, yMin: 337, xMax: 212, yMax: 373},
  {xMin:  51, yMin: 197, xMax:  95, yMax: 241},
  {xMin:  16, yMin: 105, xMax:  58, yMax: 147}
];

実のところここまでが一番面倒くさかった。

あとは、JavaScriptでCanvasを使ってプログラムを書くだけです。

ver.1

  1: <!DOCTYPE html>
  2: <html>
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>いのちの輝きくん</title>
  6: </head>
  7: <body>
  8: <h1>いのちの輝きくん</h1>
  9: <canvas id="canvas" width="358px" height="393px">
 10: </canvas>
 11: <p><a href="./index.html">戻る</p></p>
 12: <script>
 13: const canvas = document.getElementById("canvas");
 14: const context = canvas.getContext("2d");
 15: const image = new Image();
 16: image.src = "./expo_logo_no_eye.png";
 17: 
 18: const eyes = [
 19:   {xMin: 237, yMin:  18, xMax: 286, yMax:  66},
 20:   {xMin: 267, yMin: 255, xMax: 326, yMax: 309},
 21:   {xMin: 176, yMin: 337, xMax: 212, yMax: 373},
 22:   {xMin:  51, yMin: 197, xMax:  95, yMax: 241},
 23:   {xMin:  16, yMin: 105, xMax:  58, yMax: 147}
 24: ];
 25: 
 26: let degree = 0;
 27: const PUPIL_RADIUS = 10;
 28: 
 29: mainLoop();
 30: 
 31: function mainLoop() {
 32:   context.drawImage(image, 0, 0);
 33:   let radian = degree / 180 * Math.PI;
 34:   eyes.forEach(eye =>{
 35:     const hRadius = (eye.xMax - eye.xMin) / 2;
 36:     const vRadius = (eye.yMax - eye.yMin) / 2;
 37:     const centerX = eye.xMin + hRadius;
 38:     const centerY = eye.yMin + vRadius;
 39: 
 40:     context.fillStyle = "rgb(0, 104, 183)";
 41:     context.beginPath();
 42:     context.arc(centerX + Math.cos(radian) * (hRadius - PUPIL_RADIUS),
 43:                 centerY + Math.sin(radian) * (vRadius - PUPIL_RADIUS),
 44:                 PUPIL_RADIUS,
 45:                 0, Math.PI * 2);
 46:     context.fill();
 47:   });
 48:   degree += 10;
 49:   if (degree >= 360) {
 50:     degree = 0;
 51:   }
 52:   setTimeout(mainLoop, 20);
 53: }
 54: </script>
 55: </body>
 56: </html>

三角関数って役に立つよなー

ver.2

  1: <!DOCTYPE html>
  2: <html>
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>いのちの輝きくん ver.2</title>
  6: </head>
  7: <body>
  8: <h1>いのちの輝きくん ver.2</h1>
  9: <canvas id="canvas" width="358px" height="393px">
 10: </canvas>
 11: <p><a href="./index.html">戻る</p></p>
 12: <script>
 13: const canvas = document.getElementById("canvas");
 14: const context = canvas.getContext("2d");
 15: const image = new Image();
 16: image.src = "./expo_logo_no_eye.png";
 17: 
 18: const eyes = [
 19:   {xMin: 237, yMin:  18, xMax: 286, yMax:  66},
 20:   {xMin: 267, yMin: 255, xMax: 326, yMax: 309},
 21:   {xMin: 176, yMin: 337, xMax: 212, yMax: 373},
 22:   {xMin:  51, yMin: 197, xMax:  95, yMax: 241},
 23:   {xMin:  16, yMin: 105, xMax:  58, yMax: 147}
 24: ];
 25: 
 26: let targetX = 0;
 27: let targetY = 0;
 28: let xVec = 5;
 29: let yVec = 5;
 30: const PUPIL_RADIUS = 10;
 31: 
 32: mainLoop();
 33: 
 34: function mainLoop() {
 35:   context.drawImage(image, 0, 0);
 36: 
 37:   /*
 38:   context.fillStyle = "navy";
 39:   context.beginPath();
 40:   context.arc(targetX, targetY, 3,
 41:               0, Math.PI * 2);
 42:   context.fill();
 43:   */
 44: 
 45:   eyes.forEach(eye =>{
 46:     const hRadius = (eye.xMax - eye.xMin) / 2;
 47:     const vRadius = (eye.yMax - eye.yMin) / 2;
 48:     const centerX = eye.xMin + hRadius;
 49:     const centerY = eye.yMin + vRadius;
 50: 
 51:     let pupilX;
 52:     let pupilY;
 53:     if (Math.pow(targetX - centerX, 2) / Math.pow(hRadius - PUPIL_RADIUS, 2)
 54:         + Math.pow(targetY - centerY, 2) / Math.pow(vRadius - PUPIL_RADIUS, 2) < 1) {
 55:       pupilX = targetX;
 56:       pupilY = targetY;
 57:     } else {
 58:       const radian = Math.atan2(targetY - centerY, targetX - centerX);
 59:       pupilX = centerX + Math.cos(radian) * (hRadius - PUPIL_RADIUS);
 60:       pupilY = centerY + Math.sin(radian) * (vRadius - PUPIL_RADIUS);
 61:     }
 62: 
 63:     context.fillStyle = "rgb(0, 104, 183)";
 64:     context.beginPath();
 65:     context.arc(pupilX, pupilY, PUPIL_RADIUS,
 66:                 0, Math.PI * 2);
 67:     context.fill();
 68:   });
 69:   targetX += xVec;
 70:   if (targetX < 0 || targetX > canvas.width) {
 71:     xVec = -xVec;
 72:   }
 73:   targetY += yVec;
 74:   if (targetY < 0 || targetY > canvas.height) {
 75:     yVec = -yVec;
 76:   }
 77: 
 78:   setTimeout(mainLoop, 20);
 79: }
 80: </script>
 81: </body>
 82: </html>

37~43行目のコメントアウトを外すと、このページにある状態になります。

ver.3

  1: <!DOCTYPE html>
  2: <html>
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>いのちの輝きくん ver.3</title>
  6: </head>
  7: <body>
  8: <h1>いのちの輝きくん ver.3</h1>
  9: <div style="text-align:center">
 10: <canvas id="canvas" width="358px" height="393px" >
 11: </canvas>
 12: </div>
 13: <p><a href="./index.html">戻る</p></p>
 14: <script>
 15: 
 16: const canvas = document.getElementById("canvas");
 17: const context = canvas.getContext("2d");
 18: document.addEventListener("mousemove", mouseMove);
 19: const image = new Image();
 20: image.src = "./expo_logo_no_eye.png";
 21: image.onload = function() {
 22:   draw(0, 0);
 23: }
 24: 
 25: const eyes = [
 26:   {xMin: 237, yMin:  18, xMax: 286, yMax:  66},
 27:   {xMin: 267, yMin: 255, xMax: 326, yMax: 309},
 28:   {xMin: 176, yMin: 337, xMax: 212, yMax: 373},
 29:   {xMin:  51, yMin: 197, xMax:  95, yMax: 241},
 30:   {xMin:  16, yMin: 105, xMax:  58, yMax: 147}
 31: ];
 32: 
 33: const PUPIL_RADIUS = 10;
 34: 
 35: function mouseMove(e) {
 36:   const rect = canvas.getBoundingClientRect();
 37:   draw(e.clientX - rect.left, e.clientY - rect.top);
 38: }
 39: 
 40: function draw(targetX, targetY) {
 41:   context.drawImage(image, 0, 0);
 42: 
 43:   eyes.forEach(eye =>{
 44:     const hRadius = (eye.xMax - eye.xMin) / 2;
 45:     const vRadius = (eye.yMax - eye.yMin) / 2;
 46:     const centerX = eye.xMin + hRadius;
 47:     const centerY = eye.yMin + vRadius;
 48: 
 49:     let pupilX;
 50:     let pupilY;
 51:     if (Math.pow(targetX - centerX, 2) / Math.pow(hRadius - PUPIL_RADIUS, 2)
 52:         + Math.pow(targetY - centerY, 2) / Math.pow(vRadius - PUPIL_RADIUS, 2) < 1) {
 53:       pupilX = targetX;
 54:       pupilY = targetY;
 55:     } else {
 56:       const radian = Math.atan2(targetY - centerY, targetX - centerX);
 57:       pupilX = centerX + Math.cos(radian) * (hRadius - PUPIL_RADIUS);
 58:       pupilY = centerY + Math.sin(radian) * (vRadius - PUPIL_RADIUS);
 59:     }
 60: 
 61:     context.fillStyle = "rgb(0, 104, 183)";
 62:     context.beginPath();
 63:     context.arc(pupilX, pupilY, PUPIL_RADIUS,
 64:                 0, Math.PI * 2);
 65:     context.fill();
 66:   });
 67: }
 68: </script>
 69: </body>
 70: </html>

ver.2までできていれば、マウスポインタを追いかけるのも簡単です。

1点だけ気を付けるとすれば、ver.3では、瞳なしロゴを表示するところを、画像のonloadで行っています(22行目)。画像の読み込みは非同期で行われ、かつ、時間がかかるかもしれないので、onloadを使わずにそのままdrawImageすると、描画できない場合があります(できる場合もあります)。

ver.1やver.2では、アニメーションの表示のたびにこの画像を表示するので、最初の1回や2回、描画に失敗したところで「ばれない」のですが、ver.3ではユーザがマウスカーソルを動かさなければ1回しか表示しないので、このようにしています。


ひとつ上のページへ戻る | トップページへ戻る