【小学生でも作れる!】24色・太さ変更・再生機能つき「お絵かきアプリ」の作り方

ChatGPTは日々進化しています。当初は正確なプロンプトが必要でしたが、
今では簡単なプロンプトを繰り返すことで思い通りのアプリを作ることができます。
完成すると次のようなアプリができます。
完成品

お絵かきアプリoekaki

【小学生でも作れる!】24色・太さ変更・再生機能つき「お絵かきアプリ」の作り方

こんにちは!今回は メモ帳だけで作れる本格お絵かきアプリ の作り方を紹介します。必要なのはパソコンとメモ帳だけ。特別なソフトは不要です!

完成するアプリには次の機能があります。

  • 24色のカラー選択
  • ペンの太さ変更(円で太さを表示)
  • 消しゴム機能
  • リセットボタン
  • 描いた順番に再生できるアニメーション機能

小学生でもわかるように、やさしく説明していきます!


⭐ ChatGPTに出した指示

このお絵かきアプリは、次のように ChatGPT へ依頼して作りました。

ChatGPTへの指示文
「お絵描きアプリを作ってください。色は24色。文字の太さを変えられるようにしてください。太さは円で表現してください。リセットボタン、消しゴムをつけてください。書いた順番通りに再生できるようにしてください。」

ChatGPT はこの指示をもとに、必要な HTML + JavaScript コードを1つのファイルで作成してくれました。

ここからは、実際にそのアプリをメモ帳だけで作る手順を紹介します!


⭐ 1. フォルダを作ろう

まずはデスクトップに

「oekaki」

という名前のフォルダを作ってください。

この中に完成したアプリのファイルを入れます。


⭐ 2. メモ帳を開いてコードを貼りつける

① メモ帳を開きます

Windows:スタート → メモ帳
Mac:テキストエディット(※形式→プレーンテキストに変更)

② このブログの下のほうにある サンプルコードをすべてコピーして貼り付けます。

③ ファイル名をつけて保存

フォルダ「oekaki」の中に、

oekaki.html

という名前で保存しましょう。

これでアプリの準備はOKです!


⭐ 3. 保存したファイルを開いてみよう!

フォルダの中の oekaki.html をダブルクリックすると、お絵描きアプリが開きます。

パレットから色を選んだり、太さを変えたり、消しゴムを使ったりできます。

そしてすごいのが…

✨「再生」ボタンで、自分が描いた順番どおりに線が動きます!

絵がアニメーションになるので、とても楽しいです。


⭐ 4. アプリのしくみを解説!

ここからは少しだけプログラミングの説明です。むずかしい部分はスキップして大丈夫!

● 色(24色)

色のボタンを並べて、クリックすると色が変わるしくみです。

● 太さ(円の大きさで表示)

太さのボタンの中に「丸」を入れ、その大きさで太さがわかるようになっています。

● 描いた線を保存

実は、線を描くたびに

  • 太さ
  • 使ったツール(ペン or 消しゴム)
  • どこを通ったか(座標)

を記録しています。

● 再生機能

保存しておいた線を 1本ずつ、少しずつ描き直すことで再生しています。

アニメのように動くので、作っていてとても楽しい機能です!


⭐ 5. サンプルコード(コピペでOK!)

下記のコードをメモ帳に貼り付けて使ってください。
※コードは長いため、このブログの一番下に配置しています。


⭐ 6. 自分好みにアレンジしてみよう

慣れてきたら、こんなアレンジもできます。

  • 色をもっと増やす
  • 背景を黒板風にする
  • キャンバスを保存できるようにする
  • スタンプ機能を作る

プログラミングの世界は自由です!


⭐ 7. まとめ

今回のアプリは、メモ帳だけで動く本格的なお絵描きツールです。

  • 24色
  • 太さ変更
  • 消しゴム
  • リセット
  • 描いた順番どおりに再生

これだけの機能がついているのに、とてもかんたんに作れます。

ぜひ自分の好きな色やデザインに変えて、世界に一つだけのアプリを作ってみてください!


⭐ サンプルコード(完全版)

<!doctype html>
<html lang=”ja”>
<head>
<meta charset=”utf-8″ />
<meta name=”viewport” content=”width=device-width,initial-scale=1″ />
<title>お絵かきアプリ(24色・太さ・消しゴム・再生)</title>
<style>
:root{–toolbar-bg:#f3f4f6}
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,”Hiragino Kaku Gothic ProN”,Meiryo, sans-serif;margin:0;padding:12px;background:#fff}
.app{max-width:1100px;margin:0 auto;display:grid;grid-template-columns:260px 1fr;gap:12px}
.panel{background:var(–toolbar-bg);padding:12px;border-radius:10px;box-shadow:0 2px 6px rgba(0,0,0,0.06)}
h1{font-size:16px;margin:0 0 8px}
#palette{display:flex;flex-wrap:wrap;gap:8px}
.color-swatch{width:34px;height:34px;border-radius:6px;cursor:pointer;border:2px solid transparent;box-sizing:border-box}
.color-swatch.selected{outline:3px solid #111;outline-offset:1px}
.controls{display:flex;flex-direction:column;gap:10px}
.size-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.size-btn{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;width:44px;height:44px;cursor:pointer;border:1px solid #ddd;background:#fff}
.size-circle{border-radius:50%;background:#111}
.tool-btn{padding:8px 12px;border-radius:8px;border:1px solid #ccc;background:#fff;cursor:pointer}
.tool-btn.active{background:#111;color:#fff}
.actions{display:flex;gap:8px}
canvas{width:100%;height:70vh;border-radius:10px;background:#fff;display:block}
footer.note{grid-column:1/-1;text-align:center;margin-top:8px;color:#666;font-size:13px}
.top-row{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:8px}
.small{font-size:13px;color:#333}
</style>
</head>
<body>
<div class=”app”>
<div class=”panel”>
<div class=”top-row”>
<h1>ツール</h1>
<div class=”small”>色24・太さを円で表示</div>
</div>

<div class=”controls”>
<div>
<label><strong>色パレット(24色)</strong></label>
<div id=”palette” aria-label=”色パレット”></div>
</div>

<div>
<label><strong>太さ(円で表示)</strong></label>
<div class=”size-row” id=”sizeRow”></div>
</div>

<div>
<label><strong>ツール</strong></label>
<div style=”display:flex;gap:8px;margin-top:8px”>
<button id=”penBtn” class=”tool-btn active”>ペン</button>
<button id=”eraserBtn” class=”tool-btn”>消しゴム</button>
</div>
</div>

<div style=”display:flex;gap:8px;align-items:center”>
<div class=”actions”>
<button id=”resetBtn” class=”tool-btn”>リセット</button>
<button id=”replayBtn” class=”tool-btn”>再生</button>
</div>
</div>

<div style=”margin-top:6px;color:#444;font-size:13px”>ヒント:タッチでも描けます。再生は書いた順に表示します。</div>
</div>
</div>

<div class=”panel”>
<div style=”display:flex;align-items:center;justify-content:space-between;margin-bottom:8px”>
<div><strong>キャンバス</strong></div>
<div style=”font-size:13px;color:#666″>選択色:<span id=”currentColorName”>—</span> 太さ:<span id=”currentSize”>—</span></div>
</div>
<canvas id=”canvas”></canvas>
</div>

<footer class=”note”>このファイルをコピペで保存すればすぐに動きます。作成:ChatGPT</footer>
</div>

<script>
(function(){
const colors = [
‘#000000′,’#7f7f7f’,’#ffffff’,’#ff0000′,’#ff8c00′,’#ffd700′,
‘#008000′,’#00ced1′,’#0000ff’,’#8a2be2′,’#ff1493′,’#a52a2a’,
‘#f4a261′,’#2a9d8f’,’#264653′,’#e76f51′,’#b5651d’,’#d62828′,
‘#6a994e’,’#4cc9f0′,’#7209b7′,’#3a86ff’,’#ffb703′,’#9b5de5′
];

const sizes = [2,4,6,10,14,20]; // six sizes visually represented (we’ll use 6 buttons but app requested to vary thickness — displayed as circle sizes)

const paletteEl = document.getElementById(‘palette’);
const sizeRow = document.getElementById(‘sizeRow’);
const currentColorName = document.getElementById(‘currentColorName’);
const currentSizeEl = document.getElementById(‘currentSize’);

let currentColor = colors[0];
let currentSize = sizes[2];
let currentTool = ‘pen’;

// strokes: array of {tool,color,size,points:[{x,y}],timestamp}
let strokes = [];
let isDrawing=false; let currentStroke = null;

// build palette
colors.forEach((c,i)=>{
const btn = document.createElement(‘div’);
btn.className=’color-swatch’+(i===0? ‘ selected’:”);
btn.style.background=c;
btn.title=c;
btn.tabIndex=0;
btn.addEventListener(‘click’,()=>{
selectColor(c,btn);
});
btn.addEventListener(‘keypress’,(e)=>{if(e.key===’Enter’) selectColor(c,btn)});
paletteEl.appendChild(btn);
});

function selectColor(c,el){
currentColor = c; currentTool=’pen’;
document.querySelectorAll(‘.color-swatch’).forEach(x=>x.classList.remove(‘selected’));
if(el) el.classList.add(‘selected’);
penBtn.classList.add(‘active’); eraserBtn.classList.remove(‘active’);
updateStatus();
}

// build size buttons
sizes.forEach((s,idx)=>{
const btn = document.createElement(‘button’);
btn.className=’size-btn’;
btn.title = s + ‘ px’;
const inner = document.createElement(‘div’);
inner.className=’size-circle’;
const px = Math.max(6, Math.round(s*1.8));
inner.style.width = px+’px’; inner.style.height = px+’px’;
btn.appendChild(inner);
btn.addEventListener(‘click’,()=>{ selectSize(s,btn); });
sizeRow.appendChild(btn);
if(idx===2) btn.classList.add(‘selected’);
});

function selectSize(s,btn){
currentSize = s;
document.querySelectorAll(‘.size-btn’).forEach(x=>x.classList.remove(‘selected’));
btn.classList.add(‘selected’);
updateStatus();
}

const penBtn = document.getElementById(‘penBtn’);
const eraserBtn = document.getElementById(‘eraserBtn’);
penBtn.addEventListener(‘click’,()=>{currentTool=’pen’; penBtn.classList.add(‘active’); eraserBtn.classList.remove(‘active’); updateStatus();});
eraserBtn.addEventListener(‘click’,()=>{currentTool=’eraser’; eraserBtn.classList.add(‘active’); penBtn.classList.remove(‘active’); updateStatus();});

const resetBtn = document.getElementById(‘resetBtn’);
const replayBtn = document.getElementById(‘replayBtn’);

// canvas setup
const canvas = document.getElementById(‘canvas’);
const ctx = canvas.getContext(‘2d’);

function resizeCanvas(){
const rect = canvas.getBoundingClientRect();
const dpr = Math.max(window.devicePixelRatio || 1,1);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(Math.max(window.innerHeight*0.6, rect.height) * dpr);
canvas.style.height = Math.max(window.innerHeight*0.6, rect.height) + ‘px’;
ctx.setTransform(dpr,0,0,dpr,0,0);
redrawAll();
}
window.addEventListener(‘resize’,resizeCanvas);
resizeCanvas();

function getPointerPos(e){
const rect = canvas.getBoundingClientRect();
if(e.touches && e.touches[0]){
return {x: (e.touches[0].clientX – rect.left), y: (e.touches[0].clientY – rect.top)};
} else {
return {x: (e.clientX – rect.left), y: (e.clientY – rect.top)};
}
}

function startStroke(pt){
isDrawing=true;
currentStroke = {tool:currentTool,color:currentColor,size:currentSize,points:[pt],time:Date.now()};
strokes.push(currentStroke);
}
function extendStroke(pt){
if(!isDrawing || !currentStroke) return;
currentStroke.points.push(pt);
// draw incremental for live feel
drawStrokeSegment(currentStroke, currentStroke.points.length-2);
}
function endStroke(){ isDrawing=false; currentStroke=null; }

// events
canvas.addEventListener(‘mousedown’, (e)=>{ e.preventDefault(); startStroke(getPointerPos(e));});
canvas.addEventListener(‘mousemove’, (e)=>{ if(isDrawing) extendStroke(getPointerPos(e)); });
window.addEventListener(‘mouseup’, (e)=>{ if(isDrawing) endStroke(); });

// touch
canvas.addEventListener(‘touchstart’,(e)=>{ e.preventDefault(); startStroke(getPointerPos(e)); });
canvas.addEventListener(‘touchmove’,(e)=>{ e.preventDefault(); extendStroke(getPointerPos(e)); });
canvas.addEventListener(‘touchend’,(e)=>{ if(isDrawing) endStroke(); });

// drawing helpers
function drawStrokeSegment(stroke, fromIndex){
if(!stroke || stroke.points.length<2) return;
const p1 = stroke.points[fromIndex];
const p2 = stroke.points[fromIndex+1];
ctx.save();
if(stroke.tool===’eraser’){
ctx.globalCompositeOperation=’destination-out’;
ctx.lineWidth = stroke.size*2; // eraser a bit larger
ctx.strokeStyle = ‘rgba(0,0,0,1)’;
} else {
ctx.globalCompositeOperation=’source-over’;
ctx.lineWidth = stroke.size;
ctx.lineCap = ‘round’;
ctx.lineJoin = ‘round’;
ctx.strokeStyle = stroke.color;
}
ctx.beginPath();
ctx.moveTo(p1.x,p1.y);
ctx.lineTo(p2.x,p2.y);
ctx.stroke();
ctx.restore();
}

function redrawAll(){
// clear
ctx.clearRect(0,0,canvas.width,canvas.height);
// white background
const rect = canvas.getBoundingClientRect();
ctx.save();
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle=’#ffffff’;
ctx.fillRect(0,0,rect.width,rect.height);
ctx.restore();
// draw strokes
for(const stroke of strokes){
if(!stroke.points || stroke.points.length<2) continue;
if(stroke.tool===’eraser’) ctx.globalCompositeOperation=’destination-out’; else ctx.globalCompositeOperation=’source-over’;
ctx.lineWidth = (stroke.tool===’eraser’) ? stroke.size*2 : stroke.size;
ctx.lineCap=’round’; ctx.lineJoin=’round’;
ctx.strokeStyle = stroke.color;
ctx.beginPath();
ctx.moveTo(stroke.points[0].x, stroke.points[0].y);
for(let i=1;i<stroke.points.length;i++){
ctx.lineTo(stroke.points[i].x, stroke.points[i].y);
}
ctx.stroke();
}
ctx.globalCompositeOperation=’source-over’;
}

resetBtn.addEventListener(‘click’,()=>{
strokes = [];
redrawAll();
});

// Replay: draws strokes sequentially, point by point
replayBtn.addEventListener(‘click’, async ()=>{
if(strokes.length===0) return;
replayBtn.disabled = true; resetBtn.disabled = true;
// make a shallow copy to avoid interference
const copy = JSON.parse(JSON.stringify(strokes));
// clear canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
// paint white background
const rect = canvas.getBoundingClientRect();
ctx.save(); ctx.setTransform(1,0,0,1,0,0); ctx.fillStyle=’#ffffff’; ctx.fillRect(0,0,rect.width,rect.height); ctx.restore();

for(const stroke of copy){
if(!stroke.points || stroke.points.length===0) continue;
// draw first point as dot
const tool = stroke.tool;
const color = stroke.color;
const size = stroke.size;
// draw point-by-point
for(let i=0;i<stroke.points.length-1;i++){
// small delay per segment scaled by size
await new Promise(r=>setTimeout(r, Math.max(6, 12 – Math.min(8, Math.floor(size/2)))));
// draw segment i -> i+1
ctx.save();
if(tool===’eraser’){
ctx.globalCompositeOperation=’destination-out’;
ctx.lineWidth = size*2;
ctx.strokeStyle=’rgba(0,0,0,1)’;
} else {
ctx.globalCompositeOperation=’source-over’;
ctx.lineWidth = size;
ctx.strokeStyle = color;
ctx.lineCap=’round’; ctx.lineJoin=’round’;
}
ctx.beginPath();
const p1 = stroke.points[i]; const p2 = stroke.points[i+1];
ctx.moveTo(p1.x,p1.y); ctx.lineTo(p2.x,p2.y); ctx.stroke();
ctx.restore();
}
}
replayBtn.disabled=false; resetBtn.disabled=false;
});

function updateStatus(){
currentColorName.textContent = currentColor;
currentSizeEl.textContent = currentSize + ‘ px’;
}
updateStatus();

// initialize with white background
redrawAll();

})();
</script>
</body>
</html>

コメント

タイトルとURLをコピーしました