方角と高さの案内

端末を、空の決まった方角と高さへ向けてもらう画面の事例です。上の文字盤で方角を、下の弧で見上げる高さを案内します。状態ごとに何をどう伝えるかをまとめた事例です。

プレビュー

状態とバリエーション

探している

文字盤は、端末がいま向いている方角を上にして回ります。矢印は目標の方角を指し、下に「左へ 24 度」と、回る向きと角度を言葉で出します。高さは「いまの構えから上へ 8度」と出します。

ISS を探す

いま出ています

左へ 24 度

地平線から 49°、腕を伸ばして、こぶし 5 つぶん上

いまの構えから上へ 8度

現在地から。またたかない明るい点が、空を動いていきます。見えるのは約7分。

合った

方角の差が 8° 未満になると「そこです」と出し、矢印と文字盤の外周を緑にします。高さは差が 8° 未満で「その高さです」と出し、弧の線を緑にします。高さだけは、いったん合ったら差が 12° を超えるまで合ったままとします。構えた手の揺れで、合う・外れるを細かく繰り返さないためです。方角と高さは別々に判定し、別々の行に出します。1つの緑で両方を表すと、どちらが合ったのか分からなくなるためです。

ISS を探す

いま出ています

そこです

地平線から 49°、腕を伸ばして、こぶし 5 つぶん上

その高さです

現在地から。またたかない明るい点が、空を動いていきます。見えるのは約7分。

目標が昇る前

画面上部の見出しに残り時間を「あと 8分39秒」と出します。1時間より先なら「9/21 18:40 ごろ(2日後)」のように、日時と、あと何時間か何日かを出します。矢印と弧は、目標が最初に見えてくる方角と高さを指します。そこへ端末を向けて待てば、出てきた目標をそのまま追えます。いちばん高く昇るときの高さは、弧に薄い線と「最高 60°」で添えるだけにします。

ISS を探す

あと 8分39秒

左へ 24 度

現れるのは地平線から 10°、腕を伸ばして、こぶし 1 つぶん上

いまの構えから上へ 8度

現在地から。またたかない明るい点が、空を動いていきます。見えるのは約7分。

方位の誤差が大きい

端末が返す方位の誤差が ±20° を超えたときだけ、下に一言出します。「端末を大きく8の字に動かしてください。磁石の付いたカバーやペンが近いとずれます」。ふだんでも 10〜15° の誤差はあるので、いつも出すと誰も読まなくなります。

ISS を探す

あと 8分39秒

左へ 24 度

現れるのは地平線から 10°、腕を伸ばして、こぶし 1 つぶん上

いまの構えから上へ 8度

現在地から。またたかない明るい点が、空を動いていきます。見えるのは約7分。方位の誤差が大きくなっています(±35°)。端末を大きく8の字に動かしてください。磁石の付いたカバーやペンが近いとずれます。

方位を測れない端末

パソコンのように方位を測れない端末では、文字盤を北が上の図にして、「西南西の方角(図は北が上)」と言葉で伝えます。端末の向きが分からないので、「合った」はありません。

ISS を探す

あと 8分39秒

西南西の方角(図は北が上)

現れるのは地平線から 10°、腕を伸ばして、こぶし 1 つぶん上

現在地から。またたかない明るい点が、空を動いていきます。見えるのは約7分。

昇る前は、見え始めの点に端末を向けて待ちます。目標が出てきたら、そのまま矢印と弧で追います。

触覚

外では画面より空を見ています。合ったかどうかを、端末の震え方だけで分かるようにします。

いつ震え方
方角が合った重く1回
方角から外れた軽く2回
高さが合った小刻みに3回
  • 震わせるのは、合った瞬間と外れた瞬間だけです。画面は1秒に60回描き直すので、そのたびに震わせると1本の長い震えになります。
  • 強さは手で持っていると区別しにくいので、回数で見分けられるようにします。
  • 方角と高さが同時に合ったら、0.35 秒あけて順に震わせます。同時に出すと、1回の震えと区別できません。
  • 高さは、外れたときには震わせません。手の揺れで何度も震えるのを避けるためです。

外で使う前提

  • 画面を暗くするボタンと、震えのボタンを、下の左右の隅に置きます。端末を持った手の親指で押せる位置です。
  • 暗くする設定は、開くたびにオフから始めます。暗いまま閉じると、次に明るい場所で開いたとき何も見えません。震えの設定は覚えます。
  • 探しているあいだは、画面が自動で消えないようにします(ブラウザでは Screen Wake Lock API)。閉じるときは、画面の明るさを暗くする前に戻します。
  • 高さは度数だけでなく、「地平線から、腕を伸ばしてこぶし 5 つぶん上」とも言います。腕を伸ばしたこぶし1つの幅が、およそ 10° です。
  • 閉じるボタンは × だけにせず、「閉じる」と書いて大きく取ります。暗い屋外で押すためです。

決めている値

表は横にスクロールできます
プロパティ型初期値説明
方角の合う差°8差がこれ未満で「そこです」。以上で外れ。
高さの合う差°8差がこれ未満で「その高さです」。
高さの外れる差°12いったん合ったら、差がこれを超えるまで合ったまま。
誤差の一言を出す幅°±20方位の誤差がこれを超えたときだけ出す。
震えのあいだ秒0.35方角と高さが同時に合ったとき、2つめまで待つ時間。
こぶし1つ°10腕を伸ばしたこぶし1つの幅。高さを言葉にするときに使う。

使い方

"use client";

import * as React from "react";
import { Label, Slider } from "@gunjo/ui";

// 方角の差を -180〜180 に畳む(359° と 1° の差は 2°)
function turnDelta(target: number, heading: number) {
  return ((target - heading + 540) % 360) - 180;
}

const TARGET = { az: 247, el: 10 }; // 目標の方位と、地平線からの高さ
const HIT = 8; // 合ったとみなす差
const KEEP = 12; // 高さは、いったん合ったらこの差まで合ったまま

// 震えは順に出す。同時に出すと1回に感じる
let freeAt = 0;
function buzz(pattern: number[]) {
  const now = performance.now();
  const at = Math.max(now, freeAt);
  freeAt = at + pattern.reduce((a, b) => a + b, 0) + 350;
  setTimeout(() => navigator.vibrate?.(pattern), at - now);
}

export function DirectionGuide() {
  // 実機では DeviceOrientationEvent から入れる
  const [heading, setHeading] = React.useState(271);
  const [pitch, setPitch] = React.useState(2);

  const delta = turnDelta(TARGET.az, heading);
  const onAz = Math.abs(delta) < HIT;
  const dEl = TARGET.el - pitch;
  const [onEl, setOnEl] = React.useState(false);
  const nextOnEl = Math.abs(dEl) < (onEl ? KEEP : HIT);
  if (nextOnEl !== onEl) setOnEl(nextOnEl);

  const azRef = React.useRef<boolean | null>(null);
  const elRef = React.useRef(false);
  React.useEffect(() => {
    if (azRef.current !== null && onAz !== azRef.current) {
      buzz(onAz ? [30] : [10, 90, 10]); // 重く1回/軽く2回
    }
    azRef.current = onAz;
    if (!elRef.current && onEl) buzz([12, 33, 12, 33, 12]); // 小刻みに3回
    elRef.current = onEl;
  }, [onAz, onEl]);

  return (
    <div className="space-y-4">
      <p className="text-2xl font-bold">
        {onAz ? "そこです" : `${delta > 0 ? "右" : "左"}へ ${Math.round(Math.abs(delta))} 度`}
      </p>
      <p>
        {onEl ? "その高さです" : `いまの構えから${dEl > 0 ? "上" : "下"}へ ${Math.round(Math.abs(dEl))}度`}
      </p>
      <Label htmlFor="heading">端末の向き</Label>
      <Slider id="heading" min={0} max={359} value={heading} onValueChange={setHeading} />
      <Label htmlFor="pitch">端末の上端の角度</Label>
      <Slider id="pitch" min={-30} max={90} value={pitch} onValueChange={setPitch} />
    </div>
  );
}

設計の判断

  • 方角は文字盤、高さは弧と、絵を分けています。空を上から見た地図に両方を載せると、見る人が頭の中で向きを合わせ直す必要があります。
  • 高さの絵には、地面に立つ人・地平線(0°)・真上(90°)を描きます。数字だけだと、目線からの角度か、端末の傾きかが分かりません。
  • 文字盤の上を北にしないのは、外では自分の体の向きが基準になるためです。

使用コンポーネント

  • MapControlButton

    地図の上の丸いボタン。端末の向きは案内しない。

  • ScaleBar

    地図の縮尺を見せる。空の方角や高さは扱わない。

  • Toast

    一瞬で消える知らせ。合ったことは字と震えで出し続ける。

いつ・なぜ使うか(UIXHERO)

「いつ・なぜ使うか」は姉妹サイト UIXHERO の記事で解説する予定です。記事を書くまでは、起票した issue を置いています。