コンテンツへスキップ

Yamada's blog

メインメニュー
  • 汎用エクセルVBAツール
  • 動画マニュアル
  • ホーム
  • ツール
  • 【Gemini】バイブコーティングを試す|ローカルHTMLツールの作成例
  • AI
  • Gemini
  • html
  • ツール
  • 考え方

【Gemini】バイブコーティングを試す|ローカルHTMLツールの作成例

KINGKING007 2026年2月17日 18 分読み取り

女性
女性
「バイブコーティング」は、無料アカウントのGeminiでも出来ますか??
出来ますよ。
AIも種類が増えてますね。
HUNT
HUNT
女性
女性
そうなんです。
色々な種類があり便利なのですが、
使い方を把握するのは大変だなと感じています。
Iに目的と使い方を自然言語で伝えて、
生成されたHTMLを動かしながら調整すればできますよ。
試しに何か作ってみましょう!!
HUNT
HUNT

  • バイブコーティングを使って実際に作成したツール
  • 使用した環境(Windows/HTML)
  • 使用したAI
  • 動画再生・キャプチャツールの実行手順
  • メリット・デメリット
  • 利用時の注意点

目次

  • 実行手順
  • コード
  • 環境
  • 感想
  • メリット
    • スピード
    • 手軽さ
    • 抽象的な指示への対応力
  • デメリット
    • 知らないことは気づけない
    • AI利用者側のスキルが必要
  • 注意点
  • 参考サイト
  • まとめ

実行手順

1.
開発環境を準備します

  • Windows PC
  • Webブラウザ(Chrome / Edge など)
  • テキストエディタ(メモ帳やVS Codeなど

2.
AIを使ってコードを生成します。
今回は Gemini を使用しました。

3.
自然言語で要件を伝えます。
例:
「ローカルHTMLで動く動画再生ツールを作りたい」
「動画の一部を画像としてキャプチャできるようにしたい」

4.
生成されたHTMLコードを保存します。
video_capture_tool.html などの名前で保存。

5.
HTMLファイルをダブルクリックして起動します。
ローカル環境でそのまま動作します。

コード

以下は、バイブコーティングによって生成・調整した
ローカルHTMLで動作する動画再生・キャプチャツールのコードです。

Default
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>動画再生・キャプチャツール</title>
<style>
:root {
--yt-white: #ffffff;
--yt-light-gray: #f9f9f9;
--yt-border: #e5e5e5;
--yt-text: #0f0f0f;
--yt-blue: #065fd4;
--yt-red: #cc0000;
--btn-gray: #f2f2f2;
--btn-hover: #e5e5e5;
--fs-bg: rgba(0, 0, 0, 0.85);
--gap-2cm: 75px;
}
 
body {
font-family: "Roboto", "Arial", sans-serif;
margin: 0; background: var(--yt-light-gray); color: var(--yt-text);
display: flex; flex-direction: column; height: 100vh; overflow: hidden;
user-select: none;
}
 
header {
background: white; padding: 10px 5%; display: flex; align-items: center;
border-bottom: 1px solid var(--yt-border); z-index: 1000; flex-shrink: 0;
gap: 15px;
}
.header-title { font-weight: bold; font-size: 1.2em; }
 
#main-wrapper { display: flex; flex: 1; padding: 0 5%; overflow: hidden; gap: 0; }
 
#video-section { width: 70%; min-width: 320px; padding: 20px 0; display: flex; flex-direction: column; overflow-y: auto; }
 
#resizer { width: 10px; cursor: col-resize; background: transparent; transition: background 0.2s; display: flex; align-items: center; justify-content: center; }
#resizer:hover { background: var(--yt-border); }
 
#stock-section { flex: 1; min-width: 380px; padding: 20px 0; background: white; border-left: 1px solid var(--yt-border); display: flex; flex-direction: column; overflow: hidden; }
 
#player-container {
position: relative; width: 100%; aspect-ratio: 16/9; background: #000;
border-radius: 12px; overflow: hidden; cursor: grab; display: flex; align-items: center; justify-content: center;
}
#player-container:fullscreen { border-radius: 0; width: 100vw; height: 100vh; }
#main-video { width: 100%; height: 100%; transition: transform 0.05s linear; pointer-events: none; }
.seek-badge {
position: absolute; top: 50%; transform: translateY(-50%);
background: rgba(0,0,0,0.6); color: white; padding: 15px 30px;
border-radius: 50px; font-size: 24px; font-weight: bold; opacity: 0; pointer-events: none; z-index: 100;
}
#seek-l { left: 10%; } #seek-r { right: 10%; }
 
.file-info { margin: 15px 0; font-weight: bold; font-size: 1.2em; }
.controls-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.btn {
padding: 8px 16px; border-radius: 20px; border: none; background: var(--btn-gray);
cursor: pointer; font-size: 13px; font-weight: 500; transition: 0.2s; color: var(--yt-text);
display: flex; align-items: center; justify-content: center; white-space: nowrap;
}
.btn:hover { background: var(--btn-hover); }
.btn-blue { background: var(--yt-blue) !important; color: white !important; }
.btn-red { background: var(--yt-red) !important; color: white !important; }
 
.seekbar { flex: 1; height: 4px; accent-color: gray; cursor: pointer; }
 
.pop-trigger { position: relative; display: inline-block; }
.pop-menu {
display: none !important; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
background: var(--fs-bg); color: white; border-radius: 8px; padding: 10px;
flex-direction: column; gap: 8px; z-index: 2000; min-width: 120px;
}
.pop-trigger:hover .pop-menu { display: flex !important; }
.v-slider-container { display: flex; flex-direction: column; align-items: center; gap: 5px; min-width: 40px; }
.v-slider { appearance: slider-vertical; height: 100px; width: 8px; accent-color: white; }
 
#fs-controls {
position: absolute; bottom: 0; left: 0; width: 100%; padding: 20px;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
display: none; flex-direction: column; z-index: 2147483647; transition: opacity 0.5s;
box-sizing: border-box;
}
#fs-controls .btn { background: transparent; color: white; }
#fs-controls .seekbar { width: 100%; margin-bottom: 10px; }
#fs-time-display { color: white; }
 
.stock-header { padding: 0 15px 15px 15px; display: flex; flex-direction: column; gap: 10px; border-bottom: 1px solid var(--yt-border); }
#stock-list { flex: 1; overflow-y: auto; padding: 15px; display: flex; flex-direction: column; gap: 12px; }
.stock-item {
display: flex; gap: 10px; padding: 8px; border: 2px solid transparent; border-radius: 8px;
cursor: pointer; align-items: center; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.stock-item.selected { border-color: var(--yt-blue); background: #f0f7ff; }
.stock-thumb { width: 120px; aspect-ratio: 16/9; object-fit: contain; border-radius: 4px; background-color: #f0f0f0; }
 
#preview-overlay {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0,0,0,0.5); z-index: 9999; display: none; align-items: center; justify-content: center;
}
#preview-frame {
background: rgba(220, 220, 220, 0.85); padding: 40px; border-radius: 12px;
display: flex; flex-direction: column; align-items: center; border: 1px solid #999;
}
.p-body { display: flex; align-items: center; justify-content: center; }
#p-view-port {
width: 800px; max-width: 60vw; aspect-ratio: 16/9; background: #000;
overflow: hidden; position: relative; border-radius: 4px; cursor: grab;
}
#p-image { position: absolute; transform-origin: center; pointer-events: none; width: 100%; height: 100%; object-fit: contain; }
.p-btn-nav {
border: none; background: white; cursor: pointer; font-size: 24px;
width: 50px; height: 50px; border-radius: 50%; box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
 
.modal {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: white; padding: 30px; border-radius: 12px; z-index: 10001;
display: none; width: 400px; box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
</style>
</head>
<body>
 
<header>
<div class="header-title">動画再生・キャプチャツール</div>
<button class="btn" onclick="openHelp('main')">機能説明</button>
<button class="btn btn-blue" onclick="document.getElementById('file-input').click()">動画を取り込む</button>
<input type="file" id="file-input" accept="video/*" hidden onchange="loadFile(event)">
</header>
 
<div id="main-wrapper">
<div id="video-section">
<div id="player-container" onwheel="handleWheel(event, 'video')">
<video id="main-video"></video>
<div id="seek-l" class="seek-badge"></div>
<div id="seek-r" class="seek-badge"></div>
 
<div id="fs-controls">
<input type="range" class="seekbar" id="fs-seekbar" min="0" step="0.1" value="0">
<div class="controls-row">
<button class="btn" id="fs-play-btn" onclick="togglePlay()">再生</button>
<button class="btn" onclick="restart()">最初から再生</button>
<button class="btn" onclick="skip(-10)">10秒戻る</button>
<button class="btn" onclick="skip(10)">10秒進む</button>
<span id="fs-time-display">0:00 / 0:00</span>
<div class="pop-trigger">
<button class="btn" id="fs-speed-btn">1.0x</button>
<div class="pop-menu"><input type="range" min="0.25" max="3" step="0.05" oninput="changeRate(this.value)"><div id="fs-rate-text" style="text-align:center">1.00x</div><div id="fs-speed-options"></div></div>
</div>
<div class="pop-trigger">
<button class="btn" id="fs-mute-btn" onclick="toggleMute()">🔊</button>
<div class="pop-menu v-slider-container"><input type="range" class="v-slider" min="0" max="1" step="0.01" oninput="setVolume(this.value)"></div>
</div>
<button class="btn" onclick="resetZoom()">拡大・縮小リセット</button>
<div class="pop-trigger">
<button class="btn">🔍</button>
<div class="pop-menu v-slider-container"><input type="range" class="v-slider" min="0.1" max="5" step="0.1" oninput="setZoom(this.value)"></div>
</div>
<button class="btn" onclick="takeCapture()">動画から画像キャプチャ</button>
<button class="btn" onclick="toggleFS()">全画面表示の終了</button>
</div>
</div>
</div>
 
<div class="file-info" id="file-name-display">動画ファイルを読み込んでください</div>
 
<div class="controls-area">
<div class="controls-row">
<button class="btn" id="main-play-btn" onclick="togglePlay()">再生</button>
<button class="btn" onclick="restart()">最初から再生</button>
<button class="btn" onclick="skip(-10)">10秒戻る</button>
<button class="btn" onclick="skip(10)">10秒進む</button>
<input type="range" class="seekbar" id="main-seekbar" min="0" step="0.1" value="0">
<span id="main-time-display">0:00 / 0:00</span>
<div class="pop-trigger">
<button class="btn" id="main-speed-btn">1.0x</button>
<div class="pop-menu">
<input type="range" min="0.25" max="3" step="0.05" style="width:100%" oninput="changeRate(this.value)">
<div id="main-rate-text" style="text-align:center; margin-bottom:5px;">1.00x</div>
<div id="main-speed-options"></div>
</div>
</div>
<button class="btn" onclick="toggleFS()">全画面表示</button>
</div>
<div class="controls-row">
<button class="btn" id="main-mute-btn" onclick="toggleMute()" style="background:none">🔊</button>
<input type="range" id="main-volume-slider" min="0" max="1" step="0.01" value="1" style="width:33%;" oninput="setVolume(this.value)">
</div>
<div class="controls-row">
<button class="btn" onclick="resetZoom()">拡大・縮小リセット</button>
<span style="font-size:12px">ズーム: <span id="zoom-val">100</span>%</span>
<input type="range" id="main-zoom-slider" min="0.1" max="5" step="0.1" value="1" style="flex:1" oninput="setZoom(this.value)">
<button class="btn btn-blue" onclick="takeCapture()">動画から画像キャプチャ</button>
</div>
</div>
</div>
 
<div id="resizer"></div>
 
<div id="stock-section">
<div class="stock-header">
<div class="controls-row">
<button class="btn" id="all-select-btn" onclick="toggleAllSelect()" style="flex:1">全てのストック画像を選択</button>
<button class="btn" id="bulk-save-btn" onclick="saveSelected()" style="flex:0.5">保存</button>
<button class="btn" id="bulk-del-btn" onclick="deleteSelected()" style="flex:0.5">削除</button>
</div>
<button class="btn" onclick="clearStockAll()" style="width:100%; color:var(--yt-red)">ストック画像を一括削除</button>
</div>
<div id="stock-list" ondragover="event.preventDefault()" ondrop="dropStock(event)"></div>
</div>
</div>
 
<div id="preview-overlay" onclick="closePreview()">
<div id="preview-frame" onclick="event.stopPropagation()">
<div style="font-weight:bold; color:#666; margin-bottom:15px;">PREVIEW</div>
<div class="p-body">
<button class="p-btn-nav" id="p-prev-btn" onclick="movePreview(-1)">←</button>
<div style="width:var(--gap-2cm)"></div>
<div id="p-view-port" onwheel="handleWheel(event, 'preview')">
<img id="p-image">
</div>
<div style="width:var(--gap-2cm)"></div>
<button class="p-btn-nav" id="p-next-btn" onclick="movePreview(1)">→</button>
</div>
<div id="p-filename" style="margin:15px 0; font-weight:bold;"></div>
<div class="controls-row" style="width:100%; justify-content:center;">
<button class="btn" onclick="resetPZoom()">拡大・縮小リセット</button>
<span id="p-zoom-label" style="font-size:14px; margin:0 10px;">ズーム: 100%</span>
<input type="range" id="p-zoom-slider" min="0.1" max="5" step="0.1" value="1" style="width:200px" oninput="setPZoom(this.value)">
</div>
<div class="controls-row" style="margin-top:20px; gap:20px;">
<button class="btn btn-blue" onclick="savePCurrent()" style="padding:10px 30px;">保存</button>
<button class="btn btn-red" onclick="deletePCurrent()" style="padding:10px 30px;">削除</button>
<button class="btn" onclick="openHelp('preview')">機能説明</button>
<button class="btn" onclick="closePreview()" style="padding:10px 30px;">閉じる</button>
</div>
</div>
</div>
 
<div id="help-modal" class="modal">
<h3 id="help-title"></h3>
<div id="help-content" style="font-size:14px; line-height:1.6; margin-bottom:20px;"></div>
<button class="btn" onclick="closeHelp()" style="width:100%">閉じる</button>
</div>
 
<script>
const v = document.getElementById('main-video'), pBox = document.getElementById('player-container'), sList = document.getElementById('stock-list');
const pOverlay = document.getElementById('preview-overlay'), pImg = document.getElementById('p-image'), pView = document.getElementById('p-view-port');
let scale = 1, posX = 0, posY = 0, isDragging = false, startX, startY;
let pScale = 1, pPosX = 0, pPosY = 0, isPDragging = false, psX, psY;
let stockCount = 0, fsTimer, seekAcc = 0, seekTimer = null;
let currentPreviewEl = null;
 
const resizer = document.getElementById('resizer'), vSec = document.getElementById('video-section');
resizer.onmousedown = (e) => {
const onMove = (me) => {
const pct = (me.clientX / window.innerWidth) * 100;
if (pct > 20 && pct < 80) vSec.style.width = pct + '%';
};
const onUp = () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
};
 
[document.getElementById('main-speed-options'), document.getElementById('fs-speed-options')].forEach(div => {
for(let r=0.25; r<=3.0; r+=0.25) {
const opt = document.createElement('div');
opt.innerText = r.toFixed(2) + 'x';
opt.style.padding = '5px'; opt.style.cursor = 'pointer';
opt.onclick = () => changeRate(r);
div.appendChild(opt);
}
});
 
function loadFile(e) {
const file = e.target.files[0]; if(!file) return;
v.src = URL.createObjectURL(file);
document.getElementById('file-name-display').innerText = file.name;
v.pause(); resetZoom();
}
 
function togglePlay() { if(v.paused) v.play(); else v.pause(); updatePlayIcons(); }
function restart() { v.currentTime = 0; v.pause(); updatePlayIcons(); }
function skip(sec) { v.currentTime += sec; }
function updatePlayIcons() {
const txt = v.paused ? "再生" : "停止";
document.getElementById('main-play-btn').innerText = txt;
document.getElementById('fs-play-btn').innerText = txt;
}
 
function changeRate(r) {
v.playbackRate = r;
const txt = parseFloat(r).toFixed(2) + 'x';
['main','fs'].forEach(k => {
document.getElementById(`${k}-speed-btn`).innerText = txt;
document.getElementById(`${k}-rate-text`).innerText = txt;
});
}
 
function setVolume(val) { v.volume = val; v.muted = (val == 0); updateVolUI(); }
function toggleMute() { v.muted = !v.muted; updateVolUI(); }
function updateVolUI() {
const icon = v.muted || v.volume === 0 ? "🔇" : "🔊";
document.getElementById('main-mute-btn').innerText = icon;
document.getElementById('fs-mute-btn').innerText = icon;
document.getElementById('main-volume-slider').value = v.muted ? 0 : v.volume;
}
 
v.ontimeupdate = () => {
const cur = fmtT(v.currentTime), dur = fmtT(v.duration || 0);
document.getElementById('main-time-display').innerText = `${cur} / ${dur}`;
document.getElementById('fs-time-display').innerText = `${cur} / ${dur}`;
const pct = (v.currentTime / v.duration) * 100 || 0;
document.getElementById('main-seekbar').value = pct;
document.getElementById('fs-seekbar').value = pct;
};
function fmtT(s) { const min = Math.floor(s/60), sec = Math.floor(s%60); return `${min}:${sec.toString().padStart(2,'0')}`; }
const sbSync = (e) => v.currentTime = (e.target.value / 100) * v.duration;
document.getElementById('main-seekbar').oninput = sbSync;
document.getElementById('fs-seekbar').oninput = sbSync;
 
function setZoom(val) { scale = parseFloat(val); applyVTrans(); updateZoomUI(); }
function updateZoomUI() {
document.getElementById('zoom-val').innerText = Math.round(scale * 100);
document.getElementById('main-zoom-slider').value = scale;
}
function applyVTrans() { v.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`; }
function resetZoom() { posX = 0; posY = 0; setZoom(1); }
 
function handleWheel(e, type) {
e.preventDefault();
const delta = e.deltaY < 0 ? 0.1 : -0.1;
if(type === 'video') setZoom(Math.max(0.1, Math.min(scale + delta, 5)));
else setPZoom(Math.max(0.1, Math.min(pScale + delta, 5)));
}
 
pBox.onmousedown = (e) => {
if(e.target.closest('#fs-controls')) return;
isDragging = true; startX = e.clientX - posX; startY = e.clientY - posY;
};
pView.onmousedown = (e) => { isPDragging = true; psX = e.clientX - pPosX; psY = e.clientY - pPosY; };
 
window.onmousemove = (e) => {
if(isDragging) { posX = e.clientX - startX; posY = e.clientY - startY; applyVTrans(); }
if(isPDragging) { pPosX = e.clientX - psX; pPosY = e.clientY - psY; applyPTrans(); }
if(document.fullscreenElement) showFSUI();
};
window.onmouseup = () => { isDragging = false; isPDragging = false; };
 
function toggleFS() { if(!document.fullscreenElement) pBox.requestFullscreen(); else document.exitFullscreen(); }
document.onfullscreenchange = () => {
const isFS = !!document.fullscreenElement;
document.getElementById('fs-controls').style.display = isFS ? 'flex' : 'none';
};
function showFSUI() {
const ui = document.getElementById('fs-controls'); ui.style.opacity = 1;
clearTimeout(fsTimer); fsTimer = setTimeout(() => { ui.style.opacity = 0; }, 3000);
}
 
window.onkeydown = (e) => {
if(e.code === 'Space') { e.preventDefault(); togglePlay(); return; }
if(pOverlay.style.display === 'flex') {
if(e.key === 'ArrowLeft') movePreview(-1);
if(e.key === 'ArrowRight') movePreview(1);
return;
}
if(e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
const dir = e.key === 'ArrowRight' ? 1 : -1;
if((dir === 1 && seekAcc < 0) || (dir === -1 && seekAcc > 0)) resetSeek();
seekAcc += (dir * 5); v.currentTime += (dir * 5); showSeek(dir);
}
};
function showSeek(dir) {
clearTimeout(seekTimer);
const el = dir > 0 ? document.getElementById('seek-r') : document.getElementById('seek-l');
const other = dir > 0 ? document.getElementById('seek-l') : document.getElementById('seek-r');
other.style.opacity = 0; el.innerText = dir > 0 ? `+ ${seekAcc} >` : `< ${seekAcc}`;
el.style.opacity = 1; seekTimer = setTimeout(resetSeek, 1000);
}
function resetSeek() { document.getElementById('seek-l').style.opacity = 0; document.getElementById('seek-r').style.opacity = 0; seekAcc = 0; }
 
// --- 改良版キャプチャ関数 (枠外の余白を除去して動画部分のみ抽出) ---
function takeCapture() {
if (!v.src || v.src === "") return;
const containerW = pBox.clientWidth;
const containerH = pBox.clientHeight;
const vW = v.videoWidth;
const vH = v.videoHeight;
const vAspect = vW / vH;
// 1. 16:9枠の中での動画の「基本サイズ」を特定
let baseW = containerW;
let baseH = containerW / vAspect;
if (baseH > containerH) {
baseH = containerH;
baseW = containerH * vAspect;
}
 
// 2. 現在の表示スケールに基づいた「動画実描画サイズ」
const drawW = baseW * scale;
const drawH = baseH * scale;
 
// 3. 動画の「16:9枠に対する相対的な描画範囲」を計算
const drawX = (containerW / 2) + posX - (drawW / 2);
const drawY = (containerH / 2) + posY - (drawH / 2);
 
// 4. 「16:9の枠」と「動画の描画範囲」の重なり(共通部分)を求める
const intersectX = Math.max(0, drawX);
const intersectY = Math.max(0, drawY);
const intersectW = Math.min(containerW, drawX + drawW) - intersectX;
const intersectH = Math.min(containerH, drawY + drawH) - intersectY;
 
if (intersectW <= 0 || intersectH <= 0) return; // 画面外なら無視
 
// 5. 重なり部分だけのCanvasを作成
const canvas = document.createElement('canvas');
canvas.width = intersectW;
canvas.height = intersectH;
const ctx = canvas.getContext('2d');
 
// 6. 重なり部分の情報を逆算して動画から直接描画
// 動画のどの部分を切り取るか (sx, sy, sw, sh) を算出
const sScaleX = vW / drawW;
const sScaleY = vH / drawH;
const sx = (intersectX - drawX) * sScaleX;
const sy = (intersectY - drawY) * sScaleY;
const sw = intersectW * sScaleX;
const sh = intersectH * sScaleY;
 
ctx.drawImage(v, sx, sy, sw, sh, 0, 0, intersectW, intersectH);
stockCount++;
const name = "ストック" + String(stockCount).padStart(3, '0');
const data = canvas.toDataURL("image/png");
const item = document.createElement('div');
item.className = 'stock-item'; item.draggable = true;
item.id = 'stk-' + Date.now();
item.dataset.url = data; item.dataset.name = name;
item.innerHTML = `<img src="${data}" class="stock-thumb"><b>${name}</b>`;
item.onclick = () => { item.classList.toggle('selected'); updateStockBtns(); };
item.ondblclick = () => openPreview(item);
item.ondragstart = (ev) => ev.dataTransfer.setData('text', item.id);
sList.appendChild(item);
updateStockBtns();
}
 
function updateStockBtns() {
const sels = document.querySelectorAll('.stock-item.selected').length;
const total = document.querySelectorAll('.stock-item').length;
document.getElementById('bulk-save-btn').className = 'btn' + (sels > 0 ? ' btn-blue' : '');
document.getElementById('bulk-del-btn').className = 'btn' + (sels > 0 ? ' btn-red' : '');
document.getElementById('all-select-btn').innerText = (total > 0 && sels === total) ? "全選択を解除" : "全てのストック画像を選択";
}
 
function toggleAllSelect() {
const total = document.querySelectorAll('.stock-item').length;
const sels = document.querySelectorAll('.stock-item.selected').length;
const shouldSelect = (total !== sels);
document.querySelectorAll('.stock-item').forEach(i => i.classList.toggle('selected', shouldSelect));
updateStockBtns();
}
 
function saveSelected() {
document.querySelectorAll('.stock-item.selected').forEach(i => {
const link = document.createElement('a'); link.href = i.dataset.url; link.download = i.dataset.name + ".png"; link.click();
});
}
 
function deleteSelected() {
const sels = document.querySelectorAll('.stock-item.selected');
if(sels.length === 0) return;
if(confirm("選択した画像を削除しますか?")) { sels.forEach(s => s.remove()); updateStockBtns(); }
}
 
function clearStockAll() { if(confirm("全てのストック画像を削除しますか?")) { sList.innerHTML = ""; stockCount = 0; updateStockBtns(); } }
 
function dropStock(e) {
const id = e.dataTransfer.getData('text'), el = document.getElementById(id), target = e.target.closest('.stock-item');
if(target && el !== target) sList.insertBefore(el, target);
}
 
function openPreview(el) {
currentPreviewEl = el;
pImg.src = el.dataset.url;
document.getElementById('p-filename').innerText = el.dataset.name;
pOverlay.style.display = 'flex';
resetPZoom();
updatePNav();
}
function closePreview() { pOverlay.style.display = 'none'; closeHelp(); }
function setPZoom(val) {
pScale = parseFloat(val);
applyPTrans();
document.getElementById('p-zoom-slider').value = pScale;
document.getElementById('p-zoom-label').innerText = "ズーム: " + Math.round(pScale*100) + "%";
}
function applyPTrans() {
pImg.style.transform = `translate(${pPosX}px, ${pPosY}px) scale(${pScale})`;
}
function resetPZoom() { pPosX = 0; pPosY = 0; setPZoom(1); }
function movePreview(dir) {
const all = Array.from(document.querySelectorAll('.stock-item')), idx = all.indexOf(currentPreviewEl) + dir;
if(idx >= 0 && idx < all.length) openPreview(all[idx]);
}
function updatePNav() {
const all = Array.from(document.querySelectorAll('.stock-item')), idx = all.indexOf(currentPreviewEl);
document.getElementById('p-prev-btn').style.visibility = idx === 0 ? 'hidden' : 'visible';
document.getElementById('p-next-btn').style.visibility = idx === all.length - 1 ? 'hidden' : 'visible';
}
 
function savePCurrent() {
if (!currentPreviewEl) return;
const a = document.createElement('a');
a.href = currentPreviewEl.dataset.url;
a.download = currentPreviewEl.dataset.name + ".png";
a.click();
}
 
function deletePCurrent() { if(confirm("削除しますか?")) { const nxt = currentPreviewEl.nextElementSibling || currentPreviewEl.previousElementSibling; currentPreviewEl.remove(); if(nxt) openPreview(nxt); else closePreview(); updateStockBtns(); } }
 
function openHelp(mode) {
const m = document.getElementById('help-modal'), t = document.getElementById('help-title'), c = document.getElementById('help-content');
m.style.display = 'block';
if(mode === 'main') {
t.innerText = "再生画面の機能説明";
c.innerHTML = "<ul><li><b>Space:</b> 再生/停止</li><li><b>左右キー:</b> 5秒シーク</li><li><b>マウスホイール:</b> 動画の拡大・縮小</li><li><b>ドラッグ:</b> 拡大中の移動</li></ul>";
} else {
t.innerText = "プレビュー画面の機能説明";
c.innerHTML = "<ul><li><b>左右キー:</b> 前後の画像へ移動</li><li><b>ホイール/ドラッグ:</b> 画像のズームと移動(確認用)</li></ul>";
}
}
function closeHelp() { document.getElementById('help-modal').style.display = 'none'; }
</script>
 
</body>
</html>

このツールでは以下が可能です。

  • ローカル動画ファイルの再生
  • 再生位置のシーク・速度変更
  • 動画の拡大・縮小・移動
  • 表示中の動画から画像をキャプチャ
  • キャプチャ画像のストック・プレビュー・保存

環境

  • OS:Windows11
  • 言語:HTML
  • 実行環境:Webブラウザ(ローカル実行)
  • 使用AI:Gemini(無料アカウント、高速モード・思考モード)

サーバーやインストール作業は不要で、HTMLファイル1つで完結します。

感想

バイブコーティングを使うことで、
「こんなツールが欲しい」というイメージを、そのまま形にできると感じました。

特にHTML単体で完結するツールは、

  • 試作
  • 業務補助
  • 個人利用

といった用途に非常に向いているかと思います。

 

メリット

スピード

数時間レベルで実用ツールが完成します

手軽さ

環境構築不要、HTMLを開くだけで使えます

抽象的な指示への対応力

「動画を拡大できるようにしたい」
「キャプチャ画像を一覧管理したい」
といった曖昧な要望も、ある程度読み取って実装されました

 

デメリット

知らないことは気づけない

UIの細かい仕様や例外ケースは自分で気づく必要があります

AI利用者側のスキルが必要

  • HTMLやブラウザ挙動の基礎知識
  • 要件を正確に言語化する能力
  • 出力コードを理解・修正する力

注意点

  • エラーは発生します
  • コードの精度は100%ではありません
  • 将来的なブラウザ仕様変更への対応(メンテナンス)が必要です
  • 人の目による動作確認・バグ出しは必須です

参考サイト

  • Google Gemini 公式情報
  • MDN Web Docs(HTML / Video API / Canvas)

まとめ

バイブコーティングは、
「考えながら作る」ではなく「作りながら考える」開発スタイルと思います。

ローカルHTMLツールのような軽量な作成物とは特に相性が良く、
Geminiを活用することで、非エンジニアでも実用レベルに到達出来ると思います。

AIを正しく理解し、人が最終判断を行うことがポイントと思います。

 

投稿ナビゲーション

前: 【WordPress】スマホで表を横スクロールさせる方法|Gutenberg対応CSSを解説
次へ: 【Excel VBA】画像+テキストで作る検索可能なマニュアル作成ツールのご紹介(動画マニュアルの弱点を補完)

関連記事

20260217【Excel VBA】画像+テキストで作る検索可能なマニュアル作成ツールのご紹介(動画マニュアルの弱点を補完)
  • Excel・CSV
  • VBA
  • ツール

【Excel VBA】画像+テキストで作る検索可能なマニュアル作成ツールのご紹介(動画マニュアルの弱点を補完)

KINGKING007 2026年2月17日 0
20180901_001
  • 考え方

仕事の速い人のシンプルな3つの工夫

KINGKING007 2018年9月1日 0

検索

最近の投稿

  • 【Excel VBA】画像+テキストで作る検索可能なマニュアル作成ツールのご紹介(動画マニュアルの弱点を補完)
  • 【Gemini】バイブコーティングを試す|ローカルHTMLツールの作成例
  • 【WordPress】スマホで表を横スクロールさせる方法|Gutenberg対応CSSを解説
  • 【エクセルVBA】指定列で指定文字列を含む、セル個数を集計するVBA
  • 【エクセルVBA】特定の文字列を含むセルへ、一括で色を付けるVBA

中の人

山田太郎111

自分向けの備忘録も兼ねて、
業務効率化に繋がる情報を投稿中です。

カテゴリー

  • AI (1)
  • Chrome (2)
  • Excel・CSV (23)
  • Gemini (1)
  • Google (1)
  • html (1)
  • PDF (2)
  • Power Automate Desktop (1)
  • PowerPoint (3)
  • VBA (21)
  • Word (1)
  • WordPress (1)
  • スプレッドシート (1)
  • ツール (2)
  • バッチファイル (1)
  • メール術 (1)
  • 単語登録 (1)
  • 考え方 (2)

アーカイブ

当サイトについて

  • プライバシーポリシー
  • 免責事項

スポンサーリンク

スポンサーリンク

スポンサーリンク

PR005

関連記事

20260217【Excel VBA】画像+テキストで作る検索可能なマニュアル作成ツールのご紹介(動画マニュアルの弱点を補完)
  • Excel・CSV
  • VBA
  • ツール

【Excel VBA】画像+テキストで作る検索可能なマニュアル作成ツールのご紹介(動画マニュアルの弱点を補完)

KINGKING007 2026年2月17日 0
サムネイル
  • AI
  • Gemini
  • html
  • ツール
  • 考え方

【Gemini】バイブコーティングを試す|ローカルHTMLツールの作成例

KINGKING007 2026年2月17日 0
20260208サムネイル_【WordPress】スマホで表を横スクロールさせる方法|Gutenberg対応CSSを解説
  • WordPress

【WordPress】スマホで表を横スクロールさせる方法|Gutenberg対応CSSを解説

KINGKING007 2026年2月8日 0
2046_000
  • Excel・CSV
  • VBA

【エクセルVBA】指定列で指定文字列を含む、セル個数を集計するVBA

KINGKING007 2025年2月22日 0
Copyright © All rights reserved. | MoreNews by AF themes。