iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

クリスマスカードを作りましたのでお送りします。

みなさまHappy December!

どうでもいい話ですが、私は12月が1年の中で1番好きな月です。

街の雰囲気とか、冬らしさが出てきて「雪降らないかな〜」と勝手にワクワクする感じがなんともいえないのです!(先日ちょこっと降ったらしいのですが見逃しました。。キィィィ)

文化等により様々な祝い方が存在するこの時期ですが、私個人的にはクリスマスカードを送り合う文化はなんとも素敵だなぁと思います。日本でいうところの年賀状のような感覚でしょうか。

少しだけ調べてみたのですが、初めてクリスマスカードが送られたのは1843年のロンドンだそうです。(ほぅ〜)

そんなクリスマスカードも時代とともに発展し、今ではデジタルで作成しプリントされたものを送ったりなど作り方は様々。

私にも毎年送ってくれるお友達家族がいるのですが、自分がぼーっとしてるせいで時期がずれたり、送りそびれたり、、(毎年ありがとう。そしてごめんなさい。)

なので今年こそは。。

今年こそはと思ったものの、単に買って送るのもなんかなぁ、、届くまでに時間かかるしぃ、しかもアドベントカレンダーあるしぃ、なんかしたいなぁ、、

ということで、コードでクリスマスカードを作&画像にできるアプリをサクッと作りました。

(ちょっとウェブに公開する時間がなくてただの内容の紹介になってしまい大変恐縮でございます。らいへんはもっと頑張りますぅぅ)

構成

フレームワークが必要なほどでもないのですが、最近触っていたこともあり、その流れでNext.jsを使います。

npx create-next-app@latest を実行して、ちょちょい!という感じです。

設定もほぼデフォルトです。

カードのデザイン

いわばデジタルカードということで、ピクセル風にしてみました。 青空の下、クリスマスツリー並ぶ並木道です。 (クリスマスっぽくはないのですが、私、青空が好きでして)

クリスマスカードを作る

非常にシンプルなつくりなのですが、一応どんな感じか紹介させていただきます。

コンポーネントはこんな感じ

export default function Content() {
  const [message, setMessage] = useState("");

  return (
    <div className="flex justify-between h-full items-center p-10">
        <MessageInput setMessage={setMessage} />
        <MessageCard message={message} />
    </div>
  );
}

クリスマスカード本体(MessageCard)を作ります。

CSS書きたい欲が定期的に出てくるので今回で少しだけ消化します。

ページ全体のレイアウトはtailwind cssを使ったるのですが、カードの中身は純粋にCSSで書きました。(結構忘れていることも多かった気がします。。)

export default function MessageCard({ message }: { message: string }) {
  return (
    <div id="message-card" className="message-card">
      <div className="message-card-sky">
        <div className="message-card-message-area">
            {/* 入力したメッセージを表示する */}
          <p
            className={!!message ? "" : "message-empty"}
            dangerouslySetInnerHTML={{
              __html: message || "ここにメッセージが入ります",
            }}
          ></p>
        </div>
        {/* 並木道 */}
        <div className="message-card-tree-road">
          <Tree />
          <Tree />
          <Tree />
        </div>
        <div className="message-card-ground">
          <p>Happy Holidays & A Happy New Year!</p>
        </div>
      </div>
    </div>
  );
}

// 木
function Tree() {
  return (
    <div className="tree">
      <div className="tree-top">
        <div className="tree-top-star"></div>
        <div className="tree-top-leaves"></div>
        <div className="tree-top-leaves"></div>
        <div className="tree-top-leaves"></div>
      </div>
      <div className="tree-trunk"></div>
    </div>
  );
}

CSSはこんな感じ

.message-card {
  width: 400px;
  height: 550px;
  box-shadow: 5px 5px 8px 1px rgba(0, 0, 0, 0.15);
  flex-shrink: 0;
}

.message-card-message-area {
  background: rgba(255, 255, 255, 0.8);
  position: absolute;
  top: 70px;
  left: 50%;
  width: 300px;
  height: 155px;
  transform: translateX(-50%);
  border-radius: 20px;
  padding: 20px;

  p {
    font-size: 13px;
    overflow: hidden;
    height: 100%;
  }

  p.message-empty {
    color: #999999;
  }
}

.message-card-sky {
  background: linear-gradient(
    0deg,
    rgba(255, 255, 255, 1) 0%,
    rgba(209, 235, 255, 1) 100%
  );
  height: 100%;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.message-card-ground {
  background-color: #da7a2c;
  height: 200px;
  width: 100%;
  padding: 20px;
  text-align: center;

  p {
    color: #fff;
    font-family: var(--font-silkscreen);
    font-size: 30px;
    margin: 30px 0 0;
    text-align: left;
  }
}

.message-card-tree-road {
  width: 100%;
  display: flex;
  justify-content: center;
  gap: 30%;
}

.tree,
.tree-top {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.tree-top {
  position: relative;

  &:before {
    content: "";
    position: absolute;
    width: 2px;
    height: 2px;
    background: #fff;
    top: 10px;
    left: 12px;
  }

  &:after {
    content: "";
    position: absolute;
    width: 2px;
    height: 2px;
    background: #fff;
    top: 21px;
    left: 18px;
  }
}

.tree-top-star {
  width: 5px;
  height: 5px;
  background: #ffea00;
}

.tree-top-leaves {
  height: 10px;
  background: #0c9e0c;
}

.tree-top-leaves:nth-child(2) {
  width: 15px;
  height: 8px;
}

.tree-top-leaves:nth-child(3) {
  width: 23px;
  height: 8px;
}

.tree-top-leaves:nth-child(4) {
  width: 30px;
}

.tree-trunk {
  width: 8px;
  height: 10px;
  background: #d47239;
}

message部分に入力したメッセージが渡ってきてリアルタイムで反映してプレビュー表示がされます。

ではメッセージを入力するフォームとそのコンポーネント(MessageInput)を作ります。

「画像を作成&ダウンロード」ボタンをクリックすると画像が生成されてダウンロードできるイメージです。

textareaに入力した内容を親コンポーネント(Content)のstateにセットして、MessageCardにmessageを渡してる感じです。

export default function MessageInput({
  setMessage,
}: {
  setMessage: React.Dispatch<React.SetStateAction<string>>;
}) {
  function handleMessageInput(e: React.ChangeEvent<HTMLTextAreaElement>) {
    setMessage(e.target.value.replace(/\n/g, "<br />"));
  }

  // handleGenerateImage

  return (
    <div className="flex flex-col justify-center p-10 h-full items-center">
      <div className="flex flex-col">
        <p className="mb-2.5">メッセージを入力</p>
        <textarea
          className="w-96 border border-slate-400 border-solid rounded px-2.5 py-2 outline-none"
          rows={5}
          cols={50}
          onChange={(e) => handleMessageInput(e)}
        />
        <div className="text-center ">
          <button
            className="w-64 text-base font-semibold text-white py-2 bg-red-600 mt-7 rounded-3xl"
            onClick={handleGenerateImage}
          >
            画像を作成する
          </button>
        </div>
      </div>
    </div>
  );
}

こんな感じになりました。

入力フォームとカードのプレビューが並んでいます。

あとは画像変換の処理を入れていくのみです!

dom-to-imageで画像に変換する

dom-to-imageの使い方は至って簡単、nodeを引数に渡してあげるだけです。

今回はjpegに変換していますが、それ以外にもpngsvg、blob、ピクセルデータなどにも対応しているようです。

機会もなく、初めて知ったライブラリなのですが、ちょうどいい試す機会がやってきて嬉しい限りです。

  // 画像生成
  function handleGenerateImage() {
    const node = document.getElementById("message-card") as HTMLElement;
    domtoimage
      .toPng(node)
      .then(function (dataUrl) {
        const img = new Image();
        img.src = dataUrl;
        // 画像をダウンロード
        downloadImage(dataUrl);
      })
      .catch(function (error) {
        console.error(error);
      });
  }

    // 画像ダウンロードの関数
  function downloadImage(dataUrl: string) {
    const link = document.createElement("a");
    link.href = dataUrl;
    link.download = "holiday-card.png";
    link.click();
  }

これを「画像を作成する」ボタンをクリックした時に実行します。

実際に実行してみます。するとなんか、ちょっとボケてる??

調べてみるとissueを見つけました。

https://github.com/tsayen/dom-to-image/issues/332

どうやら生成される画像を大きめにすれば粗さは改善されるようです。

optionsとしてnodeと一緒に引数に渡してあげます。

 function handleGenerateImage() {
    const node = document.getElementById("message-card") as HTMLElement;
    const scale = 2;
    // 画像生成前に要素に適用されるスタイル
    const style = {
      transform: "scale(" + scale + ")",
      transformOrigin: "top left",
    };
    // 生成される画像のサイズ
    const param = {
      width: node.offsetWidth * scale,
      height: node.offsetHeight * scale,
      style,
    };
    domtoimage
      .toPng(node, param)
      .then(function (dataUrl) {
        const img = new Image();
        img.src = dataUrl;
        downloadImage(dataUrl);
      })
      .catch(function (error) {
        console.error(error);
      });
  }

すると、画像サイズは大きくなってしまったのですが、 確かに粗さは改善されたようです! 割といい感じなのでは!

クリスマスの日にでも送ってみようかと思います! みなさまにはお先に、

おわりに

本当はちゃんと公開して、もう少し動きつけて〜、GIFにして〜

などなど色々やりたかったのですが、担当の日が来てしまいました。。。悔しぃぃ〜〜

年末年始結構ガッツリお休みあるのでその期間でその辺り実現させていこうかと思います!多分!

ちょっとあまり技術感はなかったのですが、個人的には結構楽しいプロジェクトになりました。

今年も感謝!来年もどうぞよろしくお願いいたします!

明日の担当はグレートダディideさんです!

最後になりますが、現在弊社ではエンジニアを募集しています!

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

iimon採用サイト / Wantedly / Green

参考
https://www.britannica.com/topic/Christmas-card https://www.whychristmas.com/customs/christmas-cards