AIイラストの尻を撫で撫でするミニゲーム(試作)
諸君 私は尻が好きだ
諸君 私は尻が好きだ
諸君 私は尻が大好きだ
美尻が好きだ
桃尻が好きだ
小尻が好きだ
四角尻だって好きだ
デカ尻が好きだ
中くらいの尻だってもちろん好きだ
この地上に存在するありとあらゆる尻が大好きだ
理路整然と並べられた壁尻を見たときなど心がおどる
すまない…………ありとあらゆる尻が好きは言い過ぎた。やはり女の尻に限る。
というわけで、AIイラストを知ったばかりの1か月ほど前、Promptもよく理解していないのに即座に「AIイラストの尻をふりふりしたい。…したくない?」を投稿するくらいには尻が好きでございます。
アレはアレでわりと満足していて、ほんの数分で作れるチープアニメのわりにはエロくて良いと思っております。投稿するネタがなくなったら、テキトーに動く尻の量産も良いな~と思う程度には。
それはそれとして、触れる尻も欲しい。………………ラブドールがあるじゃん、と言われればそのとおりなのだけれど、それはそれ。これはこれ。別腹なのである。
AIイラストの尻を撫で撫でするミニゲーム(?)
手動で動かすとぬるぬる感がないから、微妙………か?
少なくともマウスで動かしている分にはけっこうシュールなので、投稿した後、スマホで見てみるゥ!
………………………スマホでテスト中………………………
うーん、画面狭いと動かせる範囲狭くてちょっとストレスね。
ベース絵とPrompt
尻は切り取って動かすため、ミニゲーム上では拡大して使っています。
Prompt
1girl,loli,flat chest,bronde twintail,pink cheerleader,pleated skirt,white panties,yellow pom poms
,bend over,from behind
,<lora:jyojipan_ver2_NP:1>
,best quality
,simple flat background,trouble face,blush,small smile
Negative prompt: (worst quality, low quality:1.2), monochrome, text, signature, username, blurry, bad anatomy, photorealistic, 3d,badhandv4
チアガールがcheerleaderなのは良いとして、ポンポンって英語で何て言うのかなー?と思ってググったらpom-pomsで良いらしい。なんか笑った。
技術解説
JavaScript初心者向けの解説なので、興味ない方や中級者以上はスルー推奨。
尻を手動で動かす処理
尻がひとつならタグもひとつで良いのだけれど、快感ゲージが100%になったときに隠している尻と入れ替えるので、次のようにdivタグ(id=move_object)で囲んでいる。このmove_objectのポインターイベントを書くことにより、タッチ&マウス操作に対応。
<div id="move_object">
<img src="20230424_ass_02.webp" draggable="false">
<img src="20230424_ass_02a.webp" draggable="false" id="img_mode2">
</div>
PCだけなら対応する必要はないのだけれど、スマホのタッチ操作にも対応したい場合には、まずデフォルトのスクロール動作を止めてやらないと話にならない。尻撫でるたびに画面がスクロールしてウザいので。その処理がこの部分。touchmove時にデフォルトの動作を停止しているだけ。
document.getElementById('move_object').addEventListener('touchmove', function () {
event.preventDefault();
});
次にタッチまたはマウスを左クリックした瞬間の動作。ここではタッチまたはマウスの座標を変数に保存している。
let drag_flg, drag_start_x=0,drag_start_y=0;
document.getElementById('move_object').addEventListener('pointerdown', function () {
drag_flg = true;
drag_start_x = (event.clientX - this.offsetLeft);
drag_start_y = (event.clientY - this.offsetTop);
document.getElementById('img_mode2').style.display = 'none';
});
このevent.clientX/event.clientYはマウスカーソルの絶対座標であり、尻の座標指定はスタイルシートによる相対座標指定になるため、必ず座標変換の計算が必要になる。この例では尻のマウスの絶対座標からオフセット座標を引いて、尻の左上の座標とマウスカーソル位置の相対距離をdrag_start_x/yに保存している。
実はevent.offsetX/event.offsetYというマウスカーソルの相対座標を取得するプロパティもあるのだけれど、基本的には使わないほうが良い。なぜならば、そのイベントが発生した要素の親要素から見た相対座標になるからだ。
それで良いじゃないかと思われるかもだが、HTMLの場合、なんでもかんでも入れ子にして後付けで修正することがある。設計段階ではimgタグだけで動かすつもりだったのに、これをdivタグで囲むようにしたり。やっぱもうひとつdivタグ用意しようか、とか。
そうなったとき、イベントが発生した要素のすぐ上の親からの相対(オフセット)座標だけで良いのか?ということだ。大抵問題が発生する。
悪いことは言わないので、マウスドラッグによる移動処理を実装するときはevent.clientX/clientYによる絶対座標を取得し、変数へ保存するようにしよう。仮にすぐ上の親要素からのオフセットが欲しくなっても、parentNode.getBoundingClientRectで親座標の取得なんて簡単に出来るのだから。
今回の例では下記のとおり、マウスカーソルの絶対座標から、pointerdownイベントで保存しておいた相対距離を引くことで、尻をマウスカーソルに追従させている。
document.getElementById('move_object').addEventListener('pointermove', function () {
if (drag_flg) {
x = parseInt(event.clientX - drag_start_x);
y = parseInt(event.clientY - drag_start_y);
this.style.top = y+'px';
this.style.left = x+'px';
}
});
…のだけれど、尻を動かす範囲を一定範囲に留めようとした場合、マウスの移動量についても見る必要がある。長くなるため、ここでは詳細は端折るが、pointerdown時にevent.clientXを保存しておき、pointermove時にどれだけ移動したかを見ている。具体的には(this.clientWidth*0.07)より大きく動いたかどうか。尻の大きさの7%っすね。それ以上動かすと、切り貼りしてるのが見えちゃうから、というだけの理由。
それをあたかもゲーム性と言い張るために、移動範囲外に出ようとしたときにスコアを-25点したりしている。苦肉の策である。
尻の移動範囲を制限することと、マウスから手を放す(pointerup)こと、コンテンツ外へのマウス移動(pointerleave)は基本的には同じことなので、処理も同じ。
document.getElementById('move_object').addEventListener('pointerup', function () {
drag_flg = null;
this.style.top = '';
this.style.left = '';
});
document.getElementById('move_object').addEventListener('pointerleave', function () {
drag_flg = null;
this.style.top = '';
this.style.left = '';
});
このようにドラッグ移動中を示すdrag_flg変数をnullにし、スタイルシートのtop/leftを空白にすることによって、元の位置へ戻している。ここ、わりと重要。
当サイトのようにPC/スマホの両方に対応したレスポンシブデザインの場合、スタイルシートのtop/leftはパーセンテージで指定することが多い。
しかし、スタイルシート上では尻に対してtop:40%;left:23%;という位置を指定していても、これをJavaScriptから読むことはできない。まぁ指定することは出来るのだけれど。
どちらにしろ、pointerdown/move等で取得しているマウスカーソルの座標は数値なわけで、決して割合(パーセント)ではない。この変換を自分でしようとするとクソ面倒くさいのだが、スタイルに空白を指定すれば(尻が)元に戻る、という仕様を利用して割合計算をしなおす手間を削減している。
目パチの関数化
以前「AIイラストで作った立ち絵で目パチ口パクしてみた」で作った目パチの処理だけれど、今後も使いそうなので、関数化して使いまわせるようにした。
function blinkEye(blink_speed = 100, double_blink_ratio = 50)
{
setInterval(function () {
if (Math.random() > parseFloat(double_blink_ratio / 100)) {
blink_count = 1;
} else {
blink_count = 2;
}
let eye_ms = 0;
let eye_ms_inc = blink_speed;
let eye_elms = document.getElementsByClassName('blink_eyes');
while (blink_count > 0) {
for (let i = 0; i < eye_elms.length; i++) {
setTimeout(function () { eye_elms[i].style.display = 'block'; }, eye_ms);
eye_ms += eye_ms_inc;
}
for (let i = eye_elms.length - 1; i >= 0; i--) {
setTimeout(function () { eye_elms[i].style.display = 'none'; }, eye_ms);
eye_ms += eye_ms_inc;
}
blink_count--;
}
}, 3000);
}
ふきだしの関数化
以前「AIイラストで作った立ち絵で目パチ口パクしてみた」で作った口パクの処理だけれど、今後も以下略。
time_id = null;
function talk(msg)
{
let elm = document.getElementById('balloon');
elm.innerText= msg;
elm.style.display = 'block';
if (time_id) {
clearTimeout(time_id);
}
time_id = setTimeout(function () { elm.style.display = 'none'; }, 2000);
}
前回はふきだしが消える速度が1.5秒だったのと、ボタン側で連続動作を抑制していたのだけれど、今回は「尻を動かす」というのを1ピクセル単位で検出しているため、場合によってはスコアが連続で上昇し、結果ふきだしがかぶる可能性が出てきた。
次のセリフがあるから前のセリフが消される、という動作自体は構わないのだけれど、前のセリフを表示した後、2秒経たないうちに次のセリフが表示された場合、前のセリフを消す動作で次のセリフが消えてしまうことになる。…言葉にするとわかりづれえ!!
具体的には途中経過で「あ…」とか「ん…」というセリフを言ってから2秒経たないうちにスコア100%に達する可能性があり、実際テスト中にそうなったため、二重実行時はsetTimeoutのキャンセル処理clearTimeoutを入れることにした。グローバル変数使いたくなかったんだけど、ん-、ほかに思いつかなかったのでとりあえず。
どうせセリフの長さに応じて実行時間を変える処理とか追加で作りたくなるだろうし、そのときにでも。
長押しメニューの無効化
記事をUPしてからじゃないとスマホでテストできないので、後で気が付いたのだけれど、尻が画像扱いだから(当たり前)尻に指を置いたままにするとこの画像を保存する的な長押しメニューが出てきてまともに動かなかった。
ので、取り急ぎ尻画像に長押しメニュー無効化のコードを入れた。
document.querySelectorAll('#move_object img').forEach(elm => {
elm.oncontextmenu = function () {
event.preventDefault();
}
});
まとめ
やらしく触りたい人生だった。












