public/lib/shurechara.js
キャラクター描画ライブラリ
/* ===== アバターの部品(数字で持つ。絵は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; }