public/lib/shurechara.js

キャラクター描画ライブラリ

158行 · 7,373 bytes · SHA-256 a012d11798a0e45c6af30f1f58339d3b96129be5cc80bbaea7c66d03726eb548

分割 1/1
/* ===== アバターの部品(数字で持つ。絵は1枚も要らない) ===== */
var HAIR_N = 8;                     /* 髪型8種 */
var HAIR_NM = ['短髪', 'ぼさ', 'ボブ', 'ロング', 'ポニー', 'アフロ', '三つ編み', 'つるり'];
var HAIR_COL = ['#241a12', '#4a2f1c', '#7b4a22', '#b07a35', '#d9b95e', '#8b9099', '#e9e3d3', '#b4443a'];
var SKIN_COL = ['#f7dcc2', '#eec49f', '#d29b6e', '#a97244'];
var WEAR_COL = ['#3f6fa8', '#c0563f', '#4f8a5b', '#7a5aa0', '#d3a33c', '#2f3b46', '#cc7096', '#e3ded1'];
function avDefault() {
  return { h: 0, hc: 0, sk: 0, w: 0 };
}
function avFix(a) {
  a = a || {};
  return {
    h: ((a.h | 0) % HAIR_N + HAIR_N) % HAIR_N,
    hc: ((a.hc | 0) % HAIR_COL.length + HAIR_COL.length) % HAIR_COL.length,
    sk: ((a.sk | 0) % SKIN_COL.length + SKIN_COL.length) % SKIN_COL.length,
    w: ((a.w | 0) % WEAR_COL.length + WEAR_COL.length) % WEAR_COL.length
  };
}

/* ===== 人 =====
   g … 描く先 / x,yHead … 頭の中心 / r … 頭の半径 / dir … +1で右向き(左の席) / av … アバター
   st … {mouth:0..1 口の開き, walk:0..1 歩いている, lift:0..1 腰を浮かす, gone:帰った}
   bottom … 体の下端(テーブルの高さ) */
function drawChar(g, x, yHead, r, dir, av, st, bottom) {
  av = avFix(av); st = st || {};
  var skin = SKIN_COL[av.sk], wear = WEAR_COL[av.w], hair = HAIR_COL[av.hc];
  var shY = yHead + r * 1.30;
  var bot = bottom != null ? bottom : shY + r * 2.4;
  g.save();
  g.lineCap = 'round'; g.lineJoin = 'round';

  /* 歩いている時だけ足が見える(座ったらテーブルに隠れる) */
  if (st.walk) {
    var sw = Math.sin(st.walk * Math.PI * 6);
    g.strokeStyle = '#33323a'; g.lineWidth = r * 0.28;
    g.beginPath(); g.moveTo(x, bot - r * 0.2); g.lineTo(x + sw * r * 0.55, bot + r * 1.15); g.stroke();
    g.beginPath(); g.moveTo(x, bot - r * 0.2); g.lineTo(x - sw * r * 0.55, bot + r * 1.15); g.stroke();
  }

  /* 胴。上が狭く下が広い台形 */
  g.fillStyle = wear;
  g.beginPath();
  g.moveTo(x - r * 0.74, shY);
  g.lineTo(x + r * 0.74, shY);
  g.lineTo(x + r * 1.16, bot);
  g.lineTo(x - r * 1.16, bot);
  g.closePath(); g.fill();

  /* 腕。テーブルの上に片手を置いている */
  g.strokeStyle = wear; g.lineWidth = r * 0.34;
  g.beginPath(); g.moveTo(x + dir * r * 0.62, shY + r * 0.18);
  g.lineTo(x + dir * r * 1.30, shY + r * 0.95); g.stroke();
  g.strokeStyle = skin; g.lineWidth = r * 0.26;
  g.beginPath(); g.moveTo(x + dir * r * 1.28, shY + r * 0.92);
  g.lineTo(x + dir * r * 1.80, bot - r * 0.10); g.stroke();
  g.fillStyle = skin;
  g.beginPath(); g.arc(x + dir * r * 1.80, bot - r * 0.10, r * 0.20, 0, 7); g.fill();

  /* 首 */
  g.fillStyle = skin;
  g.fillRect(x - r * 0.26, yHead + r * 0.55, r * 0.52, r * 0.90);

  /* 頭 */
  g.fillStyle = skin;
  g.beginPath(); g.arc(x, yHead, r, 0, 7); g.fill();
  /* 耳(向いている反対側) */
  g.beginPath(); g.arc(x - dir * r * 0.93, yHead + r * 0.06, r * 0.17, 0, 7); g.fill();

  /* 髪 */
  g.fillStyle = hair;
  var H = av.h;
  if (H !== 7) {  /* 7 = つるり(髪なし) */
    g.beginPath(); g.arc(x, yHead - r * 0.05, r * 1.03, Math.PI * 1.02, Math.PI * 2.04); g.fill();
  }
  if (H === 1) { /* ぼさ */
    for (var i = -3; i <= 3; i++) {
      g.beginPath(); g.arc(x + i * r * 0.30, yHead - r * 0.72 + Math.abs(i) * r * 0.12, r * 0.26, 0, 7); g.fill();
    }
  } else if (H === 2) { /* ボブ */
    g.fillRect(x - r * 1.04, yHead - r * 0.10, r * 0.30, r * 0.95);
    g.fillRect(x + r * 0.74, yHead - r * 0.10, r * 0.30, r * 0.95);
  } else if (H === 3) { /* ロング */
    g.fillRect(x - r * 1.04, yHead - r * 0.10, r * 0.32, r * 2.30);
    g.fillRect(x + r * 0.72, yHead - r * 0.10, r * 0.32, r * 2.30);
  } else if (H === 4) { /* ポニーテール(後ろへ垂らす) */
    var px = x - dir * r * 1.05;
    g.beginPath(); g.moveTo(px, yHead - r * 0.35);
    g.quadraticCurveTo(px - dir * r * 0.75, yHead + r * 0.55, px - dir * r * 0.32, yHead + r * 1.65);
    g.lineTo(px + dir * r * 0.22, yHead + r * 1.45);
    g.quadraticCurveTo(px - dir * r * 0.10, yHead + r * 0.45, px + dir * r * 0.28, yHead - r * 0.30);
    g.closePath(); g.fill();
  } else if (H === 5) { /* アフロ */
    for (var j = 0; j < 9; j++) {
      var an = Math.PI * (1.02 + j / 8 * 1.0);
      g.beginPath(); g.arc(x + Math.cos(an) * r * 0.92, yHead + Math.sin(an) * r * 0.92, r * 0.40, 0, 7); g.fill();
    }
  } else if (H === 6) { /* 三つ編み(2026-09-09 K1指示でちょんまげから差し替え) */
    var ss = [-1, 1];
    for (var q = 0; q < 2; q++) {
      var sd = ss[q], sx = x + sd * r * 0.94;
      for (var k = 0; k < 4; k++) {
        g.beginPath();
        g.arc(sx + sd * r * 0.06 * ((k % 2) ? 1 : -1), yHead + r * (0.18 + k * 0.34), r * 0.19, 0, 7);
        g.fill();
      }
    }
  } else if (H === 0) { /* 短髪 */
    g.fillRect(x - r * 1.02, yHead - r * 0.28, r * 2.04, r * 0.22);
  }

  /* 顔。向いている側に寄せる */
  var ex = x + dir * r * 0.40, ey = yHead - r * 0.10;
  g.fillStyle = '#2b2620';
  g.beginPath(); g.arc(ex, ey, r * 0.115, 0, 7); g.fill();
  g.beginPath(); g.arc(x + dir * r * 0.02, ey - r * 0.02, r * 0.10, 0, 7); g.fill();
  /* 眉 */
  g.strokeStyle = hair; g.lineWidth = r * 0.09;
  g.beginPath(); g.moveTo(ex - r * 0.16, ey - r * 0.34); g.lineTo(ex + r * 0.18, ey - r * 0.30); g.stroke();

  /* 口。食べている時だけ開く */
  var mo = st.mouth || 0;
  var mx = x + dir * r * 0.34, my = yHead + r * 0.38;
  g.fillStyle = '#7a3b33';
  if (mo > 0.04) {
    g.beginPath(); g.ellipse(mx, my, r * (0.16 + 0.20 * mo), r * (0.10 + 0.26 * mo), 0, 0, 7); g.fill();
  } else {
    g.strokeStyle = '#7a3b33'; g.lineWidth = r * 0.10;
    g.beginPath(); g.moveTo(mx - r * 0.20, my); g.lineTo(mx + r * 0.20, my); g.stroke();
  }
  g.restore();
}
/* 口の位置(食べる判定に使う)。drawChar と同じ式でなければならない */
function mouthAt(x, yHead, r, dir) { return { x: x + dir * r * 0.34, y: yHead + r * 0.38 }; }


/* ===== 外から使う口 =====
   使い方(canvasの文脈gへ1人描く):
     SHURE.draw(g, x, yHead, r, dir, {h:0,hc:0,sk:0,w:0}, {mouth:0, walk:0}, bottom);
   r=頭の半径。全部この r を基準に組んであるので、r を変えれば丸ごと拡大縮小できる。
   dir は +1 で右向き、-1 で左向き。
   av を省くと既定(短髪・黒髪・肌0・青服)。st を省くと口を閉じて座っている姿。 */
var SHURE = {
  draw: drawChar,
  mouthAt: mouthAt,
  fix: avFix,
  def: avDefault,
  HAIR_N: HAIR_N, HAIR_NM: HAIR_NM,
  HAIR_COL: HAIR_COL, SKIN_COL: SKIN_COL, WEAR_COL: WEAR_COL,
  /* 組み合わせの総数 = 8 x 8 x 4 x 8 = 2048通り */
  total: function () { return HAIR_N * HAIR_COL.length * SKIN_COL.length * WEAR_COL.length; },
  /* 番号から1人作る(同じ番号なら必ず同じ人になる) */
  fromSeed: function (n) {
    n = Math.abs(n | 0);
    return { h: n % HAIR_N, hc: (n / 8 | 0) % HAIR_COL.length, sk: (n / 64 | 0) % SKIN_COL.length, w: (n / 256 | 0) % WEAR_COL.length };
  }
};
if (typeof window !== 'undefined') { window.SHURE = SHURE; }
if (typeof module !== 'undefined' && module.exports) { module.exports = SHURE; }