絵文字スタンプを重ねてデコ&カスタマイズ!

最後に操作したスタンプを保持

view source

JavaScript

document.title = '絵文字スタンプを重ねてデコ&カスタマイズ!';

let emojis;
let activeStamp = null; // 最後に操作したスタンプを保持
let zIndexCounter = 10; // グローバルz-indexカウンター
let wasDragged = false;


const root = document.getElementById('demo');
root.style.display = 'flex';
root.style.flexDirection = 'column';
root.style.alignItems = 'center';
root.style.gap = '10px';

// Canvas領域
const canvas = document.createElement('div');
canvas.style.width = '400px';
canvas.style.height = '400px';
canvas.style.border = '1px solid #000';
canvas.style.position = 'relative';
canvas.style.overflow = 'hidden';
canvas.style.cursor = 'pointer';
root.appendChild(canvas);

const emojiBar = document.createElement('div');
root.appendChild(emojiBar);

// ▼ スライダー追加 ▼
const scaleLabel = document.createElement('div');
scaleLabel.textContent = '';
scaleLabel.style.marginTop = '5px';

const scaleInput = document.createElement('input');
root.appendChild(scaleLabel);
root.appendChild(scaleInput);
scaleInput.type = 'range';
scaleInput.min = '16';
scaleInput.max = '256';
scaleInput.value = '64';
scaleInput.style.width = '400px';
scaleInput.addEventListener('input', () => {
  if (activeStamp) {
    const prevWidth = activeStamp.offsetWidth;
    const prevHeight = activeStamp.offsetHeight;

    // サイズ変更
    activeStamp.style.fontSize = `${scaleInput.value}px`;

    // 変更後のサイズを取得
    const newWidth = activeStamp.offsetWidth;
    const newHeight = activeStamp.offsetHeight;

    // 中心を維持するために差分の半分だけ位置をずらす
    const deltaX = (prevWidth - newWidth) / 2;
    const deltaY = (prevHeight - newHeight) / 2;

    const left = parseFloat(activeStamp.style.left || 0);
    const top = parseFloat(activeStamp.style.top || 0);

    activeStamp.style.left = `${left + deltaX}px`;
    activeStamp.style.top = `${top + deltaY}px`;
  }
});

// ▼ 回転スライダー追加 ▼
const rotateLabel = document.createElement('div');
rotateLabel.textContent = '';
rotateLabel.style.marginTop = '5px';

const rotateInput = document.createElement('input');
rotateInput.type = 'range';
rotateInput.min = '-180';
rotateInput.max = '180';
rotateInput.value = '0'; // 中心がデフォ
rotateInput.style.width = '400px';

rotateInput.addEventListener('input', () => {
  if (activeStamp) {
    const angle = rotateInput.value;
    const currentScale = activeStamp.style.fontSize || '64px';

    // transformにまとめて反映(拡大と回転)
    activeStamp.style.transform = `rotate(${angle}deg)`;
  }
});

root.appendChild(rotateLabel);
root.appendChild(rotateInput);


const h1 = document.createElement('h1');
h1.textContent = document.title;
root.appendChild(h1);


const downloadBtn = document.createElement('button');
downloadBtn.textContent = 'ダウンロード';
downloadBtn.style.marginTop = '10px';

downloadBtn.addEventListener('click', () => {
  html2canvas(canvas).then(canvasImage => {
    const link = document.createElement('a');
    link.href = canvasImage.toDataURL('image/png');
    link.download = 'stamp-image.png';
    link.click();
  });
});

root.appendChild(downloadBtn);












fetch('emoji.csv')
  .then(response => response.text())
  .then(data => {
    emojis = data.split('\n').map(e => e.trim()).filter(e => e);
    setStage();
  });

function setStage(){
  // 絵文字一覧(横スクロール)
  emojiBar.style.display = 'grid';
  emojiBar.style.gridTemplateColumns = 'repeat(12, 1fr)';
  emojiBar.style.gap = '0px'; // gapなし
  emojiBar.style.fontSize = '24px';
  emojiBar.style.cursor = 'pointer';
  emojiBar.style.maxWidth = '640px';
  emojiBar.style.overflowY = 'auto';
  emojiBar.style.maxHeight = '200px'; // 必要に応じて調整
  emojiBar.style.borderTop = '1px solid #ccc';
  emojiBar.style.padding = '5px 0';

  emojiBar.style.fontSize = '24px';
  emojiBar.style.cursor = 'pointer';

  // 選択状態
  let selectedEmoji = null;

  // 絵文字ボタン生成
  emojis.forEach(emoji => {
    const btn = document.createElement('div');

    btn.textContent = emoji;
    btn.style.padding = '5px';
    btn.style.borderRadius = '5px';
    btn.style.textAlign = 'center';
    btn.style.userSelect = 'none';
    btn.style.height = '32px'; // 行の高さを統一
    btn.style.lineHeight = '32px'; // 中央寄せ


    btn.addEventListener('click', () => {
      selectedEmoji = emoji;
      // ハイライト
      [...emojiBar.children].forEach(child => child.style.background = '');
      btn.style.background = 'lightblue';
    });

    emojiBar.appendChild(btn);
  });

  // スタンプロジック
  canvas.addEventListener('click', (e) => {
    if (wasDragged) {
      wasDragged = false; // ← 一度だけ無効化
      return; // ← このクリックはドラッグ直後なので無視
    }
    if (!selectedEmoji) return;

    const clickedElement = document.elementFromPoint(e.clientX, e.clientY);
    if (clickedElement.classList.contains('emoji-stamp')) {
      // すでにドラッグ処理が始まっている場合は回転しない
      if (!clickedElement.dataset.dragging) {
        const rotation = getControlledRotation();
        clickedElement.style.transform = `rotate(${rotation}deg)`;
      }
      // ここで activeStamp に設定
      activeStamp = clickedElement;
      return;
    }

    // スタンプ追加
    const stamp = document.createElement('div');
    activeStamp = stamp; //

    stamp.textContent = selectedEmoji;
    stamp.classList.add('emoji-stamp');
    stamp.style.position = 'absolute';
    stamp.style.fontSize = `${scaleInput.value}px`;
    stamp.style.userSelect = 'none';
    stamp.style.zIndex = 1;
    stamp.style.pointerEvents = 'auto';
    stamp.style.display = 'inline-block';  // ← 必須
    stamp.style.lineHeight = '1';          // ← 高さを詰める
    stamp.style.padding = '0';             // ← 余白を消す
    stamp.style.margin = '0';              // ← 念のため
    stamp.style.whiteSpace = 'nowrap';    // ← 改行による高さ増加を防ぐ
    stamp.style.border = 'none';          // 念のため
    stamp.style.background = 'transparent'; // 念のため

    // 一時的にcanvasに追加してサイズ取得
    canvas.appendChild(stamp);
    const stampRect = stamp.getBoundingClientRect();
    const canvasRect = canvas.getBoundingClientRect();
    const offsetX = stampRect.width / 2;
    const offsetY = stampRect.height / 2;
    stamp.offsetX = offsetY;
    stamp.offsetY = offsetY;

    // left/topを中心に補正
    const x = e.clientX - canvasRect.left - offsetX;
    const y = e.clientY - canvasRect.top - offsetY;
    stamp.style.left = `${x}px`;
    stamp.style.top = `${y}px`;

    makeDraggable(stamp); // ← ドラッグ機能追加
    // ダブルクリックで削除
    stamp.addEventListener('dblclick', () => {
      stamp.remove();
    });
  });

}

// スタンプが選択されていない場合のダブルクリックで画像アップロード
canvas.addEventListener('dblclick', () => {
  if (!emojis) return; // スタンプが選択中ならスキップ

  const input = document.createElement('input');
  input.type = 'file';
  input.accept = 'image/*';
  input.style.display = 'none';

  input.onchange = (e) => {
    const file = e.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const img = document.createElement('img');
      img.src = event.target.result;
      img.style.position = 'absolute';
      img.style.left = '0';
      img.style.top = '0';
      img.style.width = '100%';
      img.style.height = '100%';
      img.style.objectFit = 'contain';
      img.style.zIndex = '0'; // 背景扱い

      canvas.appendChild(img);
    };
    reader.readAsDataURL(file);
  };

  document.body.appendChild(input);
  input.click();
  input.remove();
});


function makeDraggable(stamp) {
  let offsetX = -stamp.offsetX;
  let offsetY = -stamp.offsetY;
  let isDragging = false;

  function handleDrag(e) {
    if (!isDragging) return;

    let clientX, clientY;
    if (e.touches) {
      clientX = e.touches[0].clientX;
      clientY = e.touches[0].clientY;
    } else {
      clientX = e.clientX;
      clientY = e.clientY;
    }

    const rect = canvas.getBoundingClientRect();
    const x = clientX - rect.left - offsetX;
    const y = clientY - rect.top - offsetY;

    stamp.style.left = `${x}px`;
    stamp.style.top = `${y}px`;
  }

  function onStart(e) {
    isDragging = true;
    stamp.dataset.dragging = 'true';

    const rect = stamp.getBoundingClientRect();
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;

    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;
    const dist = Math.hypot(clientX - centerX, clientY - centerY);
    const radius = Math.min(rect.width, rect.height) / 2;
    if (dist > radius) {
      stamp.style.zIndex--;
      return; // 距離が遠い → 無視(ドラッグ開始しない)
    }
    if (e.touches) {
      offsetX = e.touches[0].clientX - rect.left;
      offsetY = e.touches[0].clientY - rect.top;
    } else {
      offsetX = e.clientX - rect.left;
      offsetY = e.clientY - rect.top;
    }

    // 最前面に
    zIndexCounter++;
    stamp.style.zIndex = zIndexCounter;

    document.addEventListener('mousemove', handleDrag);
    document.addEventListener('touchmove', handleDrag, { passive: false });
    document.addEventListener('mouseup', onEnd);
    document.addEventListener('touchend', onEnd);
  }

  function onEnd() {
    isDragging = false;
    delete stamp.dataset.dragging;
    wasDragged = true;

    document.removeEventListener('mousemove', handleDrag);
    document.removeEventListener('touchmove', handleDrag);
    document.removeEventListener('mouseup', onEnd);
    document.removeEventListener('touchend', onEnd);
  }

  stamp.addEventListener('mousedown', onStart);
  stamp.addEventListener('touchstart', onStart, { passive: false });
}




function getControlledRotation() {
  const largeRotationChance = 0.2;
  if (Math.random() < largeRotationChance) {
    // 大きく回転:±90〜±180度
    const sign = Math.random() < 0.5 ? -1 : 1;
    return sign * (90 + Math.random() * 90); // 90〜180
  } else {
    // 小さく回転:±0〜±90度
    const sign = Math.random() < 0.5 ? -1 : 1;
    return sign * Math.random() * 90;
  }
}

CSS

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/demo/-js/js-emoji-stamp-custom/

ABOUT

hi0a.com 「ひまあそび」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

仕様変更、API廃止等、実験途中で動かないページもあります。