2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Qiita全国学生対抗戦Advent Calendar 2022

Day 2

Discord.jsと@napi-rs/canvasでおみくじ画像を生成して投稿する

Last updated at Posted at 2022-12-01

まえがき

皆さんこんにちは、Fuseです。12月も始まり、年末年始が近づいてきましたね。
ところで、皆さんは年末年始といえば何を思い浮かべますか?私が思い浮かべるのは…

syougatsu2_omijikuji2.png
引用元:いらすとや様

おみくじです。
なかなか初詣に行けないこのご時世、おみくじの結果で友達とはしゃぎたくてもはしゃげない。
せめてオンラインでこの夢をかなえるべく、Discordのbotを作りました。

完成品

https://github.com/Fuses-Garage/OmikujiMion
image.png
こんな感じに運勢を占って教えてくれる。

制作環境

  • 使用言語 Node.js v16.15.0
    • Discord.js v14 Discordと連携するためのライブラリ
    • @napi-rs/canvas v0.1.30 画像を描画したり操作するライブラリ
    • express 4.18.2 稼働してるかのチェックに使ってます
  • ホスティング先兼IDE Repl.it

制作過程

1日目

image.png
背景画像と
image.png
巫女さんを用意して

(一部抜粋)ping.js
async execute(interaction) {//実行時の処理
		const canvas = createCanvas(700,250);
		const context = canvas.getContext('2d');
		const background = await loadImage('Material/ping.png');
		context.drawImage(background, 0, 0, canvas.width, canvas.height);
		context.lineWidth = 10
		context.strokeStyle = '#03a9f4'
		context.fillStyle = '#03a9f4'

		context.strokeRect(0, 0, canvas.width, canvas.height);
		const avatar = await loadImage('Material/miko8.png');
		// Move the image downwards vertically and constrain its height to 200, so that it's square
		context.drawImage(avatar, 450, 25, 200, 200);
		const attachment = new AttachmentBuilder(await canvas.encode('png'), { name: 'profile-image.png' });
		interaction.reply({ files: [attachment] });
	},

こんな感じのコードを公式ガイドとにらめっこしながら描き上げました。

2日目

(一部抜粋)ping.js
async execute(interaction) {//実行時の処理
		const canvas = createCanvas(700,250);
		const context = canvas.getContext('2d');
		const background = await loadImage('Material/ping.png');
		context.drawImage(background, 0, 0, canvas.width, canvas.height);
		context.lineWidth = 10
		context.strokeStyle = '#03a9f4'
		context.fillStyle = '#03a9f4'

		context.strokeRect(0, 0, canvas.width, canvas.height);
		const avatar = await loadImage('Material/miko8.png');
		// Move the image downwards vertically and constrain its height to 200, so that it's square
		context.drawImage(avatar, 450, 25, 200, 200);
		context.beginPath();
	// Start the arc to form a circle
		context.arc(70, 450, 100, 0, Math.PI * 2, true);
	// Put the pen down
		context.closePath();
	// Clip off the region yo//u drew on
		//context.clip();
		context.font = '60px MigMix 1M';
		context.fillStyle = '#ffffff';
		// Select the style that will be used to fill the text in
		
		// Draw a rectangle with the dimensions of the entire canvas
		
		// Actually fill the text with a solid color
		context.fillText('Pong!', canvas.width / 2.5, canvas.height / 1.8);
		const attachment = new AttachmentBuilder(await canvas.encode('png'), { name: 'profile-image.png' });
		interaction.reply({ files: [attachment] });
	},

こんな感じのコードを書き上げました。
ここで注意したいのは、context.clip();は、自分の直前ではなく自分より後の描画をクリッピングすると言うことですね。
制作当時はそれに気づかず文字が表示されないと大ハマり。
ドキュメントはちゃんと読むべきですね。

3日目

lgtm.js
const { SlashCommandBuilder, AttachmentBuilder } = require('discord.js');
const { createCanvas, loadImage } = require('@napi-rs/canvas');
const { GlobalFonts } = require('@napi-rs/canvas');
GlobalFonts.registerFromPath
	('./Material/migmix/migmix-1m-bold.ttf', 'migm');

module.exports = {
	data: new SlashCommandBuilder()//コマンドのデータ
		.setName('lgtm')//コマンドの識別名(一意)
		.setDescription('送られた画像をLGTM画像に加工します。')//コマンドの説明文
	    .addAttachmentOption(option =>
		option.setName('元画像')
			.setDescription('加工元の画像をアップロードしてください')
			.setRequired(true)),
	
	async execute(interaction) {//実行時の処理
		const file =interaction.options.getAttachment("元画像")

		if (!file) return // 添付ファイルがなかったらスルー
		if (!file.height && !file.width) return // 画像じゃなかったらスルー
		const canvas = createCanvas(file.width, file.height);//画像のファイルに合わせキャンバス生成
		const context = canvas.getContext('2d');
		const background = await loadImage(file.url);//背景として読み込み
		context.drawImage(background, 0, 0, canvas.width, canvas.height);//渡された画像を背景に
		context.font = Math.ceil(Math.min(canvas.width * 0.8 / 4, canvas.height * 0.8)).toString() + 'px MigMix 1M';
		context.fillStyle = '#ffffff';//色は白
		context.textAlign = 'center'
		context.textBaseline = 'middle'
		context.fillText('LGTM', canvas.width / 2, canvas.height / 2);
		//縦横ともに中央揃え
		const attachment = new AttachmentBuilder(await canvas.encode('png'), { name: 'profile-image.png' });
		//メッセージに添付できる形式に変換
		interaction.reply({ files: [attachment] });//画像を添付し返信
	},
};

今回は趣向を変えて、与えられた画像の加工に挑戦してみました。
与えられた画像からLGTM画像を生成するコマンドです。
与えられた画像をinteraction.options.getAttachment("元画像")で読み取り加工します。

4日目

clipの仕様を再確認し、丸い枠に顔アイコンを表示する処理を作りました。

5日目

serif.js
const { createCanvas ,loadImage} = require('@napi-rs/canvas');
const {GlobalFonts} = require('@napi-rs/canvas');
GlobalFonts.registerFromPath
('./Material/migmix/migmix-1m-bold.ttf', 'migm');
async function Serif(fukicenter,fukisize,fukiradius,text,faceradius,facetype,context,haba) {
		context.font = '30px MigMix 1M';//フォント読み込み
		context.lineWidth = 5//外枠の太さ
		context.textAlign = 'center'
		context.textBaseline = 'middle'
		context.save();
		context.beginPath();//パス定義ここから
		var lu={"x":fukicenter["x"]-fukisize["x"]/2,"y":fukicenter["y"]-fukisize["y"]/2}
		var ru={"x":fukicenter["x"]+fukisize["x"]/2,"y":fukicenter["y"]-fukisize["y"]/2}
		var rd={"x":fukicenter["x"]+fukisize["x"]/2,"y":fukicenter["y"]+fukisize["y"]/2}
		var ld={"x":fukicenter["x"]-fukisize["x"]/2,"y":fukicenter["y"]+fukisize["y"]/2}
		context.moveTo(lu["x"]+fukiradius,lu["y"]);
		context.lineTo(ru["x"]-fukiradius,ru["y"]);
		context.arc(ru["x"]-fukiradius,ru["y"]+fukiradius,fukiradius, Math.PI*-0.5, 0, false);//円を描く
		context.lineTo(rd["x"],fukicenter["y"]-20);
		context.lineTo(rd["x"]+haba,fukicenter["y"]);
		context.lineTo(rd["x"],fukicenter["y"]+20);
		context.lineTo(rd["x"],rd["y"]-fukiradius);
		context.arc(rd["x"]-fukiradius,rd["y"]-fukiradius,fukiradius, 0, Math.PI * 0.5, false);//円を描く
		context.lineTo(ld["x"]+fukiradius,ld["y"]);
		context.arc(ld["x"]+fukiradius,rd["y"]-fukiradius,fukiradius,  Math.PI * 0.5, Math.PI * 1, false);//円を描く
		context.lineTo(lu["x"],lu["y"]+fukiradius);
		context.arc(lu["x"]+fukiradius,lu["y"]+fukiradius,fukiradius, Math.PI*1, Math.PI * 1.5, false);//円を描く
		context.closePath();//パス定義終わり
		context.strokeStyle = '#303030'
		context.stroke()//パスを描画
		context.clip();//ここからはパスの中にのみ描画
		context.fillStyle = '#606060';//灰色
		context.fill();//パスを塗りつぶす
		context.fillStyle = '#ffffff';//文字は白色
		context.fillText(text, fukicenter["x"], fukicenter["y"]);
		context.restore();
		context.beginPath();//パス定義ここから
		context.arc(fukicenter["x"]+fukisize["x"]/2+faceradius+haba, fukicenter["y"], faceradius, 0, Math.PI * 2, true);//円を描く
		context.closePath();//パス定義終わり
		
		context.strokeStyle = '#303030'
		context.stroke()//パスを描画
		context.clip();//ここからはパスの中にのみ描画
		context.fillStyle = '#606060';//灰色
		context.fill();//パスを塗りつぶす
		const avatar = await loadImage('Material/miko'+facetype+'.png');//ミコちゃんを描画
		context.drawImage(avatar, fukicenter["x"]+fukisize["x"]/2-faceradius*1+haba, fukicenter["y"]-faceradius*2, faceradius*4, faceradius*4);
}
module.exports = Serif;

ここから開発を進めるに当たり、よく使うことになるであろう吹き出しの描画処理を作成し、
今までのping.jsの内容と一緒に別ファイルに分離しました。

6日目

omikuji.js
const { SlashCommandBuilder,AttachmentBuilder } = require('discord.js');
const { createCanvas ,loadImage} = require('@napi-rs/canvas');
const {GlobalFonts} = require('@napi-rs/canvas');
GlobalFonts.registerFromPath
('./Material/migmix/migmix-1m-bold.ttf', 'migm');

module.exports = {
	data: new SlashCommandBuilder()//コマンドのデータ
		.setName('omikuji')//コマンドの識別名(一意)
		.setDescription('おみくじで遊べますよ!'),//コマンドの説明文
	async execute(interaction) {//実行時の処理
		const canvas = createCanvas(700,600);
		const context = canvas.getContext('2d');
		const background = await loadImage('Material/ping.png');
		context.drawImage(background, 0, 0, canvas.width, canvas.height);
		context.lineWidth = 10//外枠の太さ
		context.strokeStyle = '#606060'
		context.strokeRect(0, 0, canvas.width, canvas.height)//外枠を描画
		const luck={1:"大凶",2:"",3:"末吉",4:"小吉",5:"",6:"中吉",7:"大吉",8:"極吉"}
		const soe=Math.floor(Math.random()*8)+1
		const lucklogo = await loadImage('Material/luck/luck'+soe+'.png');
		context.drawImage(lucklogo, 0, 0, 700, 350);
		const Serif=require("../func/serif.js");
		await Serif({"x":225,"y":475},{"x":400,"y":175},20,"今日の運勢は"+luck[soe]+"ですよ!",100,soe,context,50)
		const attachment = new AttachmentBuilder(await canvas.encode('png'), { name: 'profile-image.png' });
		interaction.reply({ files: [attachment] });
	},
};

ping.jsをベースにおみくじを引く処理を作成。
台詞の処理を分離したことにより、大分スマートになりました。

あとがき

確率が均等だったり吉兆の表示のみだったりと
気になる点はまだまだありますが、おみくじbotを作るにあたって最低限の処理を実装することができました。
ソースコードを公開しておりますので、
皆さんもおみくじ巫女さんと楽しい年末年始を過ごしてみませんか?

クレジット

かわいい巫女さんの画像:CHARAT様
MigMixフォント:itouhiro様

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?