JavaScriptで精液ジェネレーターを作ってみた
以前、Pixivの【備忘録】精液講座を拝見し、たった3色、黒(影)/黄ばんだ白(精液)/白(光沢)と消しゴムで描けるのか!と感動しました。
うちはAIイラストが基本なので、cumと一言Promptに入れるだけで精液は描写してくれますが、ドピュっと飛び出す射精アニメーション、ドロッと流れるぶっかけアニメーションを作るためには自分で描けたほうが便利なんですよね。
GIMPで精液を描く実例(ノーカット1分30秒)
音声なし。
このようにとても簡単に描けますし、最初は楽しんで描いていたのですが、1か月間毎日Pixivへアニメーションを投稿していたら、なんだかこの単純作業が面倒になってきました。
JavaScriptで精液ジェネレーターを作ってみた(試作品)
絵の中をクリック(タップ)すると、そこへ向けて射精します。ワリと重い処理をしているので、環境によっては描画まで数秒かかります。
[精液クリア]ボタンで描画した精液をすべて消せます。
[ダウンロード]ボタンを押すと、精液だけが描写された透過PNGをダウンロードできます。
発射角が鋭角になると精液が球体になるのは(今のところ)そういう仕様です。
制作メモ
最初はアニメーションWebPや動画を生成できる精液ジェネレーターを作るつもりだったのですが、実際作ってみると動きに違和感しかなく、精液に関する知識が圧倒的に不足していることに気が付かされました。
まず、精液の発射速度ですが、調べてみると時速48km/h(=1330cm/s)あるそうです。
そして精液の飛距離はおよそ17~25cm。
仮に、精液が21cm飛翔した場合、21cm÷1330cm=約0.016秒で射精は完了しています。
およそ60分の1秒ですから60fpsでもわずか1フレーム。実際、過去にいくつも射精アニメーションを作っていますが、9フレームの射精モーションは妙に遅い気がしていました。
もちろんアニメーションですから、なにもかも現実に合わせる必要はありません。そんなこと言ったら、眼球だけで頭部の大部分を占めてしまい、脳みそが入る余地のないデフォルメ瞳のアニメ顔はどうなんだって話になるわけで、射精についても人としてはありえん量を発射しようが、速度が遅かろうが、それは演出として優れているのなら、それで構いません。
だがしかし、ぼくにはそんな演出力はないため、ある程度は物理法則に沿った作り方をしないと無限におかしくなってしまうことに気が付きました。
具体的には左から右にスィーーーっと等速で進む精液、上下にランダムに揺れ続ける精液、ヘビのようにウネウネとうねる精液、そんな精液ジェネレーターが出来上がりました。
だめだ。こんなんじゃまったくダメだ。
というわけで、いったん完成した射精アニメーションジェネレーターは廃棄し、基本に戻って、まずは普通の射精の静止画を出力する機能に絞って考えてみました。
普通の射精とはいったいなんぞや。発射地点から目標地点へ向かって放物線上に発射される精液。ただ、それだけ。
それに加えて、ダマ(精液の塊)の概念を取り入れます。
「射精時の筋収縮間隔と回数」の調査によると、射精時には前立腺周辺の筋収縮運動によって精液が排出されるわけですが、球海綿体筋を観測したところ、およそ14回もの収縮運動をしているのだとか。
そのうち、実際に精液の射出を伴う収縮が約10~12回ほどとのこと。つまり、ポンプを10回くらいキュッキュッと押すようなイメージでしょうか。そのときに粘性のある液体が後ろから「おい、はやく進めよ」と押し出されることでダマが出来る、という仕組みのようです。
では10個のダマを描写すれば良いのか? 実際やってみましたが、それだとどうにも違和感があります。1回だけビュっと出して終わるパターンというのは考えづらく、だいたいは2~3回にわけてピュッピュッと射出されるのではないでしょうか。
つまり、10÷3でダマは3個くらい…? 何本精液を描写するかにもよると思うので、そこはパラメーターで設定できるようにしておきました。
JavaScriptとcanvasの技術解説
最初に紹介したとおりの方法、つまり3色、黒(影)/黄ばんだ白(精液)/白(光沢)で精液を描画しています。既存の画像を呼び出したりとか変形させたりとかそういったことは一切していません。
プログラマー向けに言うと ctx.strokeだけで描いています。
//影描画
ctx.strokeStyle = 'rgba(110,80,0,0.1)';
ctx.filter = 'blur(1px)';
ctx.lineCap = 'round';
for (let i = 0; i < this.posArray.length; i++) {
let pos = this.posArray[i];
ctx.beginPath();
ctx.lineWidth = pos.r * 2;
ctx.moveTo(this.x + pos.x, this.y + pos.y + 4);
ctx.lineTo(this.x + pos.x, this.y + pos.y);
ctx.stroke();
}
アニメーションで作っていたときの名残りで、無駄に重い処理の原因なのですが、開始座標~目標座標までのX/Y座標を配列にすべて持っています。最大で1000ですが、飛距離分しか保存しないし、ダマの部分は半径の長さだけ省略されるので実際に使うのは500配列くらいかな。
ベジェ曲線(canvasのbezierCurveTo関数)使ったほうが早いし綺麗なのだけれど、後述する光沢と半透明消しゴムをどうしたものか悩ましかったので、結局LineToでゴリ押し。そのうち直すかも。
開始地点の円の半径は20を固定とし、徐々に半径を小さくしつつ、Y座標は疑似的な重力加速度(二次曲線)で下がっていくようにしています。
ダマの発生座標は精液描写前に乱数で生成しており、描写直前にまた乱数でダマの大きさをランダムで決めています。いきなり大きくなったり、鋭角に大きくなると違和感があるため、ここも二次曲線で大きく/小さくなるように組んでいます。
こうして配列にX/Y座標とダマの半径を格納しておくことにより、アニメーション時にダマのサイズごとに重力加速度を調整して、落下速度を分ける、という作り方をしていましたが、ここではまだ実装していません。
光沢や半透明消しゴムについても実は単純で、配列内に格納してある半径(変数r)の値が大きければダマだと判断できるため、そのダマが小さくなりはじめるときに、小さい白色を描き足しているだけです。
if (pos.r > 2 && pos.r <= prevR || i == this.posArray.length - 1) {
//光沢用の白
ctx.strokeStyle = 'rgba(255,255,255,1)';
ctx.beginPath();
ctx.moveTo(this.x + pos.x, this.y + pos.y - pos.r/2);
ctx.lineTo(this.x + pos.x, this.y + pos.y - pos.r/2);
ctx.lineWidth = pos.r*1.2;
ctx.lineCap = 'round';
ctx.stroke();
//透明部分の描画
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.globalCompositeOperation = 'destination-out';
ctx.moveTo(this.x + pos.x - (rand(1,pos.r*2)) , this.y + pos.y);
ctx.lineTo(this.x + pos.x, this.y + pos.y);
ctx.lineWidth = pos.r/2;
ctx.lineCap = 'round';
ctx.stroke();
ctx.globalCompositeOperation = 'source-over';
}
うっすらと透ける描写は精液には必須だと思うので、strokeStyleに0.5の半透明スタイルを適用し、globalCompositeOperationにdestination-outを指定することで、ダマのちょっと左を消しゴムツールでうっすらと消したような感じにしています。
処理的にはとても重いので、アニメーション化するときには、数枚分の画像データをメモリ中に作成し、完成した画像をパラパラっと切り替える感じで作ろうかなと思うちょります。















