小説投稿サイト
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>小説投稿サイト</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
max-width: 800px;
margin: auto;
background: #f2f2f2;
}
h1 {
text-align: center;
}
form {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
input, textarea {
width: 100%;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
}
button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.post {
background: white;
padding: 15px;
border-left: 5px solid #007bff;
margin-bottom: 20px;
border-radius: 5px;
}
.post h2 {
margin: 0 0 10px;
}
.meta {
color: gray;
font-size: 0.9em;
margin-bottom: 10px;
}
.delete-btn {
background-color: #dc3545;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
float: right;
cursor: pointer;
}
</style>
</head>
<body>
<h1>小説投稿サイト</h1>
<form id="novelForm">
<input type="text" id="author" placeholder="著者名" required>
<input type="text" id="title" placeholder="タイトル" required>
<textarea id="content" rows="8" placeholder="本文" required></textarea>
<button type="submit">投稿する</button>
</form>
<div id="postList"></div>
<script>
const form = document.getElementById('novelForm');
const postList = document.getElementById('postList');
let posts = JSON.parse(localStorage.getItem('novels')) || [];
function saveAndRender() {
localStorage.setItem('novels', JSON.stringify(posts));
renderPosts();
}
function renderPosts() {
postList.innerHTML = '';
[...posts].reverse().forEach((post, index) => {
const div = document.createElement('div');
div.className = 'post';
div.innerHTML = `
<button class="delete-btn" onclick="deletePost(${index})">削除</button>
<h2>${post.title}</h2>
<div class="meta">著者: ${post.author} | 投稿日: ${post.date}</div>
<p>${post.content.replace(/\n/g, '<br>')}</p>
`;
postList.appendChild(div);
});
}
form.addEventListener('submit', e => {
e.preventDefault();
const title = document.getElementById('title').value;
const content = document.getElementById('content').value;
const author = document.getElementById('author').value;
const date = new Date().toLocaleString();
posts.push({ title, content, author, date });
form.reset();
saveAndRender();
});
window.deletePost = function(index) {
posts.splice(posts.length - 1 - index, 1); // reverseしてるため
saveAndRender();
}
renderPosts();
</script>
</body>
</html>
フルダイブVR企画書
フルダイブVR企画書
企画名
フルダイブVRプロジェクト『NeoReal Dive(仮)』
企画概要
本プロジェクトは、脳と直接接続することで完全没入型の仮想現実体験(フルダイブVR)を実現することを目的とした、次世代VRプラットフォームの研究・開発・商用展開である。現行のHMD型VRを超越し、「五感の再現」「意識同期」「自由行動」の3要素を備えた、完全な仮想体験を提供する。
目的・背景
- 現在のVRは視覚・聴覚中心で、身体感覚・触覚・嗅覚などの再現が困難。
- 未来型のエンタメ・教育・医療・ビジネスにおいて、より高精度な仮想体験のニーズが高まっている。
- フルダイブVRは、脳波・神経インターフェース技術を応用することで「仮想世界での実体験」を可能にする。
目標
- 脳波インターフェースによる身体操作・五感再現システムの実装
- 仮想世界での自由移動・対話・感情表現が可能なAI/物理エンジンの開発
- フルダイブVR体験デモ版(プロトタイプ)を2年以内に完成
- エンタメ分野に限らず、医療・教育・研究機関への応用を展開
コンセプトアート/ビジュアル
※必要に応じて追加可能(仮想世界のイメージ、ユーザーの視点、デバイス外観など)
想定利用シーン
- フルダイブVR MMORPGゲーム
- リモート教育:歴史・宇宙体験・医療トレーニング
- 治療支援:リハビリ、精神ケア、PTSD治療など
- 働き方改革:完全仮想空間でのオフィス、コラボレーション
想定ターゲット
- ゲーム・VR愛好者(16〜40代)
- 研究機関、医療機関、教育機関
- メタバースビジネス参入企業
技術構成
- 脳波・神経インターフェース:BCI(Brain Computer Interface)を利用
- 五感再現:視覚・聴覚はHMD、触覚はハプティクス、嗅覚/味覚は化学刺激/脳信号への変換を検討
- 仮想世界の構築:Unreal Engine 5 / Unity + カスタムAIエンジン
- AI・NPCとの対話:GPT系AI + 音声合成エンジン
- データセキュリティ:ブロックチェーン技術または生体認証を採用
スケジュール(例)
フェーズ | 期間 | 内容 |
---|---|---|
企画・調査 | 0〜3ヶ月 | 技術調査・資金調達・提携交渉 |
開発準備 | 3〜6ヶ月 | プロトタイプ設計、チーム編成 |
開発第1段階 | 6〜12ヶ月 | 脳波制御・仮想環境ベース構築 |
開発第2段階 | 12〜18ヶ月 | 五感拡張、AI対話・感情再現 |
検証・試験 | 18〜24ヶ月 | ユーザーテスト・安全性評価 |
商用化準備 | 24ヶ月〜 | 市場投入計画、各分野への展開 |
予算案(概算)
- 研究開発費:¥500,000,000
- デバイス設計・試作:¥200,000,000
- プロモーション・展示会等:¥50,000,000
- 合計:¥750,000,000(初期段階)
パートナー候補
- 大学(脳科学・電気通信系)
- 医療機関・リハビリセンター
- 大手ゲーム会社
- ハードウェア開発企業(神経インターフェース)
将来的展望
- フルダイブによる「デジタルライフ」体験の一般化
- 仮想世界での経済活動(メタバース経済の拡大)
- 高齢者・障害者の社会参加支援
- 意識のデジタル保存や、死後世界への応用研究も視野に
その他
- 倫理的配慮:過剰依存・仮想と現実の区別問題への対策が必要
- 法整備:脳直結型デバイスに関する法規制との整合性
- 安全設計:睡眠中・長時間使用への対応、強制ログアウト機能など
必要であれば、プレゼン資料用スライド形式やPDF化もできます。また、内容を「ゲーム中心」「教育・医療応用中心」「メタバースビジネス中心」などに寄せたバージョンも可能です。どの方向性にしたいか教えてもらえますか?
4o
Javascript 迷路
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>My Maze</title>
</head>
<body>
<canvas>
Canvas not supported ...
</canvas>
<script src="js/main.js"></script>
</body>
</html>
main.js
'use strict';
(() => {
class MazeRenderer {
constructor(canvas) {
this.ctx = canvas.getContext('2d');
this.WALL_SIZE = 10;
}
render(data) {
canvas.height = data.length * this.WALL_SIZE;
canvas.width = data[0].length * this.WALL_SIZE;
for (let row = 0; row < data.length; row++) {
for (let col = 0; col < data[0].length; col++) {
if (data[row][col] === 1) {
this.ctx.fillRect(
col * this.WALL_SIZE,
row * this.WALL_SIZE,
this.WALL_SIZE,
this.WALL_SIZE
);
}
}
}
}
}
class Maze {
constructor(row, col, renderer) {
if (row < 5 || col < 5 || row % 2 === 0 || col % 2 === 0) {
alert('Size not valid!');
return;
}
this.renderer = renderer;
this.row = row;
this.col = col;
this.data = this.getData();
}
getData() {
const data = [];
for (let row = 0; row < this.row; row++) {
data[row] = [];
for (let col = 0; col < this.col; col++) {
data[row][col] = 1;
}
}
for (let row = 1; row < this.row - 1; row++) {
for (let col = 1; col < this.col - 1; col++) {
data[row][col] = 0;
}
}
for (let row = 2; row < this.row - 2; row += 2) {
for (let col = 2; col < this.col - 2; col += 2) {
data[row][col] = 1;
}
}
for (let row = 2; row < this.row - 2; row += 2) {
for (let col = 2; col < this.col - 2; col += 2) {
let destRow;
let destCol;
do {
const dir = row === 2 ?
Math.floor(Math.random() * 4) :
Math.floor(Math.random() * 3) + 1;
switch (dir) {
case 0: // up
destRow = row - 1;
destCol = col;
break;
case 1: // down
destRow = row + 1;
destCol = col;
break;
case 2: // left
destRow = row;
destCol = col - 1;
break;
case 3: // right
destRow = row;
destCol = col + 1;
break;
}
} while (data[destRow][destCol] === 1);
data[destRow][destCol] = 1;
}
}
return data;
}
render() {
this.renderer.render(this.data);
}
}
const canvas = document.querySelector('canvas');
if (typeof canvas.getContext === 'undefined') {
return;
}
const maze = new Maze(21, 15, new MazeRenderer(canvas));
maze.render();
})();
クロノクロス リメイク企画書(提案書)
クロノクロス リメイク企画書(提案書)
■ タイトル(仮)
CHRONO CROSS Re:Dreamers(クロノクロス リ・ドリーマーズ)
■ 開発目的
- 名作『クロノクロス』(1999年/PS)の世界観・物語・音楽を継承しつつ、現代の技術と表現力でフルリメイク。
- クロノシリーズの価値とブランドを再定義し、次世代のファンを獲得する。
- クロノ・トリガーから続く「時」と「次元」をテーマにした壮大な物語を、新たな感動体験として再構築。
■ ターゲット層
- 30〜40代:オリジナルファン(ノスタルジー層)
- 10〜20代:JRPG・アニメ調ゲームに興味がある若年層
- 世界市場向け:海外人気も高いため、グローバル対応必須(字幕・音声)
■ 主な特徴
項目 | 内容 |
---|---|
グラフィック | Unreal Engine 5を使用したセルルック風3D |
サウンド | 全曲アレンジ+原曲切替可能/フルオーケストラ対応 |
ボイス | 主要キャラクターにフルボイス対応(ON/OFF可) |
UI | モダン+クラシック切替可能なデザイン |
バトル | ターン制+リアルタイム演出のハイブリッドバトル |
クロス要素 | 40人以上の仲間、選択によるマルチストーリー |
新要素 | 新規シナリオ分岐、外伝ストーリー、キャラエピソード |
■ ストーリー概要(簡易)
夢を旅する少年セルジュが、もう一つの世界で自らの存在が「死んでいたこと」を知る。
交錯する次元、因果のねじれ、「時を喰らうもの」によって歪められた歴史を、仲間たちとともに解き明かす物語。
『クロノ・トリガー』との繋がりも明確に描かれ、真実のエンディングへ導かれる。
■ プラットフォーム案
- PS5 / Xbox Series X|S / PC(Steam / Epic) / Nintendo Switch 2(次世代機を想定)
- クラウド対応 / Steam Deck対応予定
■ 追加要素・リメイク特有要素(例)
種別 | 内容 |
---|---|
DLC対応 | クロノトリガーエピソード、旧キャラコスチュームなど |
クロスセーブ | 複数プラットフォームでの共有セーブ |
ギャラリーモード | アート、BGM視聴、ボイス再生可能なコレクション |
フォトモード | アングル調整+フィルターありの撮影機能 |
難易度設定 | イージー〜クラシック(敵の強化・MP制限など) |
■ 開発スケジュール案(例)
期間 | 内容 |
---|---|
Q1〜Q2 | プロトタイプ制作・初期アート制作 |
Q3〜Q4 | メイン開発・音楽収録・シナリオ検証 |
Q5〜Q6 | ベータ版、デバッグ、調整、プロモーション |
Q7 | グローバルリリース(発売時期例:2027年冬) |
■ 予算感(概算・中規模プロジェクト)
- 総開発費:約15〜25億円(3年開発・UE5・全ボイス)
- 人員:50〜70名体制(内外注含む)
■ 参考資料
- クロノクロスHDリマスター(2022)
- ファイナルファンタジーVII リメイク
- ライブアライブHD-2D
- ゼノブレイドシリーズ(シナリオ設計・多人数管理)
■ 最後に
クロノクロスは「ゲーム音楽」「次元の物語」「美しいドットと詩的なセリフ」で多くのファンの心に残る名作。
本リメイクは、単なる懐古主義ではなく、「再構築」と「夢の継承」をテーマに、今の時代に語り直すことを目指す。
必要であれば、PDF書式の企画書風に整えることも可能だし、ゲーム画面のモックアップや仲間キャラ一覧風資料も作れるよ!
もっと深く踏み込みたい部分ある?(キャラ紹介・UI案・音楽面とか)
4o
MySQL IF CASE
DROP TABLE IF EXISTS posts;
CREATE TABLE posts (
id INT NOT NULL AUTO_INCREMENT,
message VARCHAR(140),
likes INT,
area VARCHAR(20),
PRIMARY KEY (id)
);
INSERT INTO posts (message, likes, area) VALUES
('post-1', 12, 'Tokyo'),
('post-2', 8, 'Fukuoka'),
('post-3', 11, 'Tokyo'),
('post-4', 3, 'Osaka'),
('post-5', 8, 'Tokyo'),
('post-6', 9, 'Osaka'),
('post-7', 4, 'Tokyo'),
('post-8', 10, 'Osaka'),
('post-9', 31, 'Fukuoka');
WEBOS
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>仮想OS Pro</title>
<style>
body {
margin: 0;
background: #2c3e50;
font-family: 'Segoe UI', sans-serif;
overflow: hidden;
}
#desktop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 40px;
background: linear-gradient(#2980b9, #34495e);
}
.icon {
width: 70px;
text-align: center;
margin: 20px;
cursor: pointer;
color: white;
}
.window {
position: absolute;
width: 300px;
height: 200px;
background: white;
border: 2px solid #555;
display: none;
box-shadow: 4px 4px 10px rgba(0,0,0,0.5);
}
.window-header {
background: #3498db;
padding: 5px;
cursor: move;
color: white;
}
.window-body {
padding: 10px;
}
#taskbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: #2c3e50;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
color: white;
}
</style>
</head>
<body onload="playStartupSound(); updateClock(); setInterval(updateClock, 1000);">
<div id="desktop">
<div class="icon" onclick="openWindow('memo')">
<br>メモ帳</div>
<div class="icon" onclick="openWindow('calc')">
<br>電卓</div>
</div>
<div id="taskbar">
<div>仮想OS Pro</div>
<div id="clock"></div>
</div>
<!-- メモ帳 -->
<div class="window" id="memo">
<div class="window-header" onmousedown="dragWindow(event, this.parentElement)">メモ帳</div>
<div class="window-body">
<textarea style="width: 100%; height: 100px;">メモを入力してください</textarea>
</div>
</div>
<!-- 電卓 -->
<div class="window" id="calc">
<div class="window-header" onmousedown="dragWindow(event, this.parentElement)">電卓</div>
<div class="window-body">
<input type="text" id="calcDisplay" style="width:100%; font-size: 1.2em;" />
<button onclick="calculate()">計算</button>
</div>
</div>
<!-- 起動音 -->
<audio id="bootSound" src="https://upload.wikimedia.org/wikipedia/commons/2/2f/Windows_95_startup.ogg" preload="auto"></audio>
<script>
function openWindow(id) {
document.getElementById(id).style.display = 'block';
}
function dragWindow(e, el) {
e.preventDefault();
let offsetX = e.clientX - el.offsetLeft;
let offsetY = e.clientY - el.offsetTop;
function move(e) {
el.style.left = (e.clientX - offsetX) + 'px';
el.style.top = (e.clientY - offsetY) + 'px';
}
function stop() {
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', stop);
}
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', stop);
}
function calculate() {
let result;
try {
result = eval(document.getElementById('calcDisplay').value);
} catch {
result = "エラー";
}
document.getElementById('calcDisplay').value = result;
}
function playStartupSound() {
document.getElementById("bootSound").play();
}
function updateClock() {
const now = new Date();
const time = now.toLocaleTimeString();
document.getElementById("clock").textContent = time;
}
</script>
</body>
</html>
Twitter風サイト
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Twitter風サイト(高度拡張版)</title>
<style>
/* 全体設定 */
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #f5f8fa;
}
header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 50px;
background-color: #1da1f2;
display: flex;
align-items: center;
padding: 0 20px;
color: #fff;
font-size: 20px;
font-weight: bold;
box-sizing: border-box;
z-index: 10;
}
/* レイアウト用コンテナ */
.container {
display: flex;
width: 100%;
max-width: 1200px;
margin: 60px auto 0; /* ヘッダー分だけ下に余白をとる */
box-sizing: border-box;
}
/* 左サイドバー(ナビゲーション) */
.sidebar {
width: 20%;
max-width: 200px;
padding: 10px;
box-sizing: border-box;
}
.nav-item {
margin: 10px 0;
font-size: 18px;
}
.nav-item a {
text-decoration: none;
color: #1da1f2;
cursor: pointer;
}
.profile-settings {
margin-top: 20px;
padding: 10px;
background-color: #fff;
border: 1px solid #e6ecf0;
border-radius: 5px;
}
.profile-settings input {
width: 100%;
margin-bottom: 5px;
font-size: 14px;
padding: 5px;
box-sizing: border-box;
}
.profile-settings button {
border: none;
background-color: #1da1f2;
color: #fff;
font-size: 14px;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
/* メインタイムライン部分 */
.feed {
width: 60%;
padding: 10px;
box-sizing: border-box;
}
.tweet-box {
background-color: #fff;
border: 1px solid #e6ecf0;
border-radius: 5px;
padding: 10px;
margin-bottom: 20px;
}
.tweet-box textarea {
width: 100%;
border: none;
resize: none;
font-size: 16px;
outline: none;
box-sizing: border-box;
}
.tweet-stats {
display: flex;
justify-content: space-between;
margin-top: 5px;
font-size: 14px;
}
.tweet-stats .char-count {
color: #657786;
}
.tweet-stats .error {
color: red;
}
.tweet-box .attach-label {
display: inline-block;
margin-top: 5px;
font-size: 14px;
color: #657786;
}
.tweet-box button {
margin-top: 10px;
padding: 8px 16px;
border: none;
background-color: #1da1f2;
color: #fff;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
}
.tweet {
background-color: #fff;
border: 1px solid #e6ecf0;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
.tweet-header {
display: flex;
align-items: center;
margin-bottom: 5px;
flex-wrap: wrap;
}
.tweet-header img.user-icon {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.tweet-header .name {
font-weight: bold;
margin-right: 5px;
}
.tweet-header .username {
color: #657786;
font-size: 14px;
margin-right: 5px;
}
.tweet-time {
font-size: 12px;
color: #657786;
}
.tweet-content {
font-size: 16px;
margin: 10px 0;
white-space: pre-wrap; /* 改行を保持 */
}
.tweet-content img.attached-image {
max-width: 100%;
display: block;
margin-top: 5px;
border: 1px solid #ccc;
}
.tweet-footer {
display: flex;
justify-content: flex-start;
gap: 15px;
margin-top: 10px;
flex-wrap: wrap;
}
.tweet-footer button {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
color: #657786;
display: flex;
align-items: center;
gap: 5px;
}
.tweet .replies-container {
margin-top: 10px;
border-left: 2px solid #e6ecf0;
padding-left: 10px;
}
/* リプライの更なる階層は少しずつ左にずらす */
.nested-reply {
margin-left: 20px;
}
/* 右サイドバー(ウィジェット) */
.widgets {
width: 20%;
max-width: 250px;
padding: 10px;
box-sizing: border-box;
}
.search-box {
background-color: #fff;
border-radius: 20px;
padding: 8px 15px;
margin-bottom: 20px;
border: 1px solid #e6ecf0;
display: flex;
align-items: center;
}
.search-box input {
border: none;
outline: none;
width: 100%;
font-size: 16px;
}
.trends {
background-color: #fff;
border: 1px solid #e6ecf0;
border-radius: 5px;
padding: 10px;
}
.trends h3 {
margin-top: 0;
}
.trend-item {
margin-bottom: 10px;
font-size: 14px;
}
/* リンク風のテキストデザイン */
.hashtag,
.mention {
color: #1da1f2;
text-decoration: none;
cursor: pointer;
}
.hashtag:hover,
.mention:hover {
text-decoration: underline;
}
/* 折りたたみ表示のボタン */
.toggle-replies-btn {
background: none;
color: #1da1f2;
border: none;
cursor: pointer;
font-size: 14px;
margin-top: 5px;
padding: 0;
}
</style>
</head>
<body>
<!-- ヘッダー -->
<header>
Twitter風サイト(高度拡張版)
</header>
<!-- メインコンテンツを左右に分けるコンテナ -->
<div class="container">
<!-- 左サイドバー -->
<aside class="sidebar">
<div class="nav-item"><a href="#">ホーム</a></div>
<div class="nav-item"><a href="#">通知</a></div>
<div class="nav-item"><a href="#">設定</a></div>
<!-- 簡易プロフィール設定 -->
<div class="profile-settings">
<label for="displayName">名前</label>
<input type="text" id="displayName" placeholder="あなたの表示名">
<label for="userName">ユーザー名(@なしで)</label>
<input type="text" id="userName" placeholder="myAccount">
<button id="saveProfileBtn">保存</button>
</div>
</aside>
<!-- タイムライン部分 -->
<main class="feed">
<!-- 新規ツイート入力フォーム -->
<div class="tweet-box">
<textarea rows="3" placeholder="いまどうしてる? (140文字まで)"></textarea>
<div class="tweet-stats">
<span class="char-count">0 / 140</span>
<span class="error"></span>
</div>
<label class="attach-label">
画像を添付:
<input type="file" class="attach-input" accept="image/*">
</label>
<button class="tweet-submit-btn">ツイート</button>
</div>
</main>
<!-- 右サイドバー -->
<aside class="widgets">
<!-- 検索ボックス -->
<div class="search-box">
<input type="text" placeholder="キーワード検索">
</div>
<!-- トレンド表示 -->
<div class="trends">
<h3>今どうしてる?</h3>
<div class="trend-item">#春の訪れ</div>
<div class="trend-item">#お花見</div>
<div class="trend-item">#新年度</div>
</div>
</aside>
</div>
<script>
// =======================
// 定数・変数設定
// =======================
const TWEET_MAX_LENGTH = 140;
const feedContainer = document.querySelector('.feed');
const tweetTextarea = document.querySelector('.tweet-box textarea');
const tweetButton = document.querySelector('.tweet-submit-btn');
const charCountEl = document.querySelector('.char-count');
const errorEl = document.querySelector('.error');
const attachInput = document.querySelector('.attach-input');
const displayNameInput = document.getElementById('displayName');
const userNameInput = document.getElementById('userName');
const saveProfileBtn = document.getElementById('saveProfileBtn');
// LocalStorageからツイート一覧を読み込み(なければ空配列)
let tweets = JSON.parse(localStorage.getItem('tweets-advanced') || '[]');
// ユーザープロフィール情報をLocalStorageから読み込み
let userProfile = JSON.parse(localStorage.getItem('userProfile-advanced') || '{}');
let currentName = userProfile.displayName || 'あなた';
let currentUserName = userProfile.userName || 'myAccount';
// フォームに反映
displayNameInput.value = currentName;
userNameInput.value = currentUserName;
// =======================
// 画像ファイル取得用
// =======================
let attachedImageBase64 = null;
attachInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if(!file) {
attachedImageBase64 = null;
return;
}
const reader = new FileReader();
reader.onload = () => {
attachedImageBase64 = reader.result; // base64データ
};
reader.readAsDataURL(file);
});
// =======================
// スレッド実装のためのツイート構造
// =======================
// tweet = {
// id: string (一意のID),
// name: string,
// userName: string,
// content: string,
// time: number (Date.now()),
// likes: number,
// retweets: number,
// image: string (base64) | null,
// replies: array of same structure
// }
// =======================
// ユーティリティ
// =======================
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// ハッシュタグ/メンションのリンク化
function linkify(text) {
let escaped = escapeHtml(text);
escaped = escaped.replace(/#(\w+)/g, `<a href="#" class="hashtag">#$1</a>`);
escaped = escaped.replace(/@(\w+)/g, `<a href="#" class="mention">@$1</a>`);
return escaped;
}
// ツイートをLocalStorageに保存
function updateLocalStorage() {
localStorage.setItem('tweets-advanced', JSON.stringify(tweets));
}
// 親ツイートまたはリプライ先を検索するための再帰関数
function findTweetById(tweetArray, tweetId) {
for (const tw of tweetArray) {
if (tw.id === tweetId) {
return tw;
}
const childFound = findTweetById(tw.replies, tweetId);
if (childFound) {
return childFound;
}
}
return null;
}
// 新しいツイートを作成 & tweets配列に登録
// parentIdが指定されたら、そのツイートのrepliesに追加する
function createNewTweet(content, parentId = null, imageBase64 = null) {
const newTweet = {
id: 'tw-' + Date.now() + '-' + Math.floor(Math.random() * 10000),
name: currentName,
userName: currentUserName,
content: content,
time: Date.now(),
likes: 0,
retweets: 0,
image: imageBase64,
replies: []
};
if (parentId) {
const parentTweet = findTweetById(tweets, parentId);
if (parentTweet) {
parentTweet.replies.unshift(newTweet);
}
} else {
tweets.unshift(newTweet);
}
updateLocalStorage();
renderTweets();
}
// =======================
// ツイート描画
// =======================
// ツイート1件を生成するDOM要素を返す(返信分も含め再帰的に生成)
// depth: スレッドの深さに応じて左マージンなどを調整したいときに利用
function createTweetElement(tweet, depth = 0) {
const tweetDiv = document.createElement('div');
tweetDiv.classList.add('tweet');
if (depth >= 1) {
// 2段目以降のリプライならclassで左にずらす
tweetDiv.classList.add('nested-reply');
}
// 日付文字列
const timeString = new Date(tweet.time).toLocaleString('ja-JP', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
// 本文のリンク化
const contentHtml = linkify(tweet.content);
// 画像がある場合
const imageHtml = tweet.image
? `<img src="${tweet.image}" alt="Attached Image" class="attached-image" />`
: '';
// 自分のツイートなら削除ボタンを表示
const isMyTweet = (tweet.name === currentName && tweet.userName === currentUserName);
const deleteBtnHtml = isMyTweet
? `<button class="delete-btn">削除</button>`
: '';
tweetDiv.innerHTML = `
<div class="tweet-header">
<img src="https://via.placeholder.com/40" alt="User Icon" class="user-icon" />
<span class="name">${escapeHtml(tweet.name)}</span>
<span class="username">@${escapeHtml(tweet.userName)}</span>
<span class="tweet-time">- ${timeString}</span>
</div>
<div class="tweet-content">
${contentHtml}
${imageHtml}
</div>
<div class="tweet-footer">
<button class="like-btn">
<span>いいね</span>
<span class="like-count">${tweet.likes}</span>
</button>
<button class="retweet-btn">
<span>リツイート</span>
<span class="retweet-count">${tweet.retweets}</span>
</button>
<button class="reply-btn">返信</button>
${deleteBtnHtml}
</div>
`;
// ---------- 返信フォーム & スレッド表示 ----------
// 返信コンテナ(折りたたみ対象)
const repliesContainer = document.createElement('div');
repliesContainer.classList.add('replies-container');
// 返信がある場合、表示/非表示を切り替えるボタンを設置
if (tweet.replies && tweet.replies.length > 0) {
const toggleRepliesBtn = document.createElement('button');
toggleRepliesBtn.classList.add('toggle-replies-btn');
toggleRepliesBtn.textContent = `返信を表示 (${tweet.replies.length})`;
tweetDiv.appendChild(toggleRepliesBtn);
// 折りたたみ状態管理
let isRepliesOpen = false;
toggleRepliesBtn.addEventListener('click', () => {
isRepliesOpen = !isRepliesOpen;
toggleRepliesBtn.textContent = isRepliesOpen
? `返信を非表示`
: `返信を表示 (${tweet.replies.length})`;
repliesContainer.style.display = isRepliesOpen ? 'block' : 'none';
});
}
// 返信フォーム
const replyForm = document.createElement('div');
replyForm.style.marginTop = '5px';
replyForm.innerHTML = `
<textarea rows="2" placeholder="返信を入力..." style="width: 100%; font-size:14px;"></textarea>
<button class="reply-submit-btn" style="margin-top:5px;">返信を投稿</button>
`;
replyForm.style.display = 'none'; // デフォルトは非表示
tweetDiv.appendChild(replyForm);
// スレッド(返信)の再帰描画
tweet.replies.forEach(replyTweet => {
const replyEl = createTweetElement(replyTweet, depth + 1);
repliesContainer.appendChild(replyEl);
});
repliesContainer.style.display = 'none'; // 最初は折りたたみ
tweetDiv.appendChild(repliesContainer);
// ========== 各種ボタンイベント ==========
// いいね
const likeBtn = tweetDiv.querySelector('.like-btn');
const likeCountEl = tweetDiv.querySelector('.like-count');
likeBtn.addEventListener('click', () => {
tweet.likes++;
updateLocalStorage();
likeCountEl.textContent = tweet.likes;
});
// リツイート
const retweetBtn = tweetDiv.querySelector('.retweet-btn');
const retweetCountEl = tweetDiv.querySelector('.retweet-count');
retweetBtn.addEventListener('click', () => {
tweet.retweets++;
updateLocalStorage();
retweetCountEl.textContent = tweet.retweets;
});
// 返信ボタン -> フォーム表示/非表示
const replyBtn = tweetDiv.querySelector('.reply-btn');
replyBtn.addEventListener('click', () => {
replyForm.style.display = (replyForm.style.display === 'none') ? 'block' : 'none';
});
// 返信投稿
const replySubmitBtn = replyForm.querySelector('.reply-submit-btn');
const replyTextarea = replyForm.querySelector('textarea');
replySubmitBtn.addEventListener('click', () => {
const replyText = replyTextarea.value.trim();
if (replyText === '' || replyText.length > TWEET_MAX_LENGTH) {
return;
}
// 新規リプライ作成
createNewTweet(replyText, tweet.id);
replyTextarea.value = '';
});
// 削除
if (isMyTweet) {
const deleteBtn = tweetDiv.querySelector('.delete-btn');
deleteBtn.addEventListener('click', () => {
// 再帰的に探して削除
removeTweetById(tweets, tweet.id);
updateLocalStorage();
renderTweets();
});
}
return tweetDiv;
}
// ツイート削除(再帰)
function removeTweetById(tweetArray, tweetId) {
for (let i = 0; i < tweetArray.length; i++) {
if (tweetArray[i].id === tweetId) {
tweetArray.splice(i, 1);
return true;
}
if (removeTweetById(tweetArray[i].replies, tweetId)) {
return true;
}
}
return false;
}
// 画面上のツイート一覧を再描画
function renderTweets() {
// まず既存ツイートを全削除
const oldTweets = feedContainer.querySelectorAll('.tweet');
oldTweets.forEach(t => t.remove());
// 上から順にツイートを追加
tweets.forEach(tweet => {
const tweetEl = createTweetElement(tweet);
feedContainer.appendChild(tweetEl);
});
}
// ======================
// イベントリスナー
// ======================
window.addEventListener('DOMContentLoaded', () => {
renderTweets();
updateCharCount();
});
// ツイート文字数カウント
tweetTextarea.addEventListener('input', updateCharCount);
function updateCharCount() {
const length = tweetTextarea.value.length;
charCountEl.textContent = `${length} / ${TWEET_MAX_LENGTH}`;
if (length > TWEET_MAX_LENGTH) {
errorEl.textContent = '文字数オーバーです!';
tweetButton.disabled = true;
} else {
errorEl.textContent = '';
tweetButton.disabled = false;
}
}
// ツイート投稿
tweetButton.addEventListener('click', () => {
const text = tweetTextarea.value.trim();
if (text === '' || text.length > TWEET_MAX_LENGTH) {
return;
}
createNewTweet(text, null, attachedImageBase64);
tweetTextarea.value = '';
attachedImageBase64 = null;
attachInput.value = ''; // ファイル選択をクリア
updateCharCount();
});
// プロフィール情報の保存
saveProfileBtn.addEventListener('click', () => {
currentName = displayNameInput.value.trim() || 'あなた';
currentUserName = userNameInput.value.trim() || 'myAccount';
userProfile = {
displayName: currentName,
userName: currentUserName
};
localStorage.setItem('userProfile-advanced', JSON.stringify(userProfile));
alert('プロフィールを保存しました!\n' +
`名前:${currentName}\nユーザー名:@${currentUserName}`);
renderTweets(); // 表示名を変えて再描画
});
</script>
</body>
</html>
RPG
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>レトロRPG</title>
<style>
body {
background: black;
color: white;
text-align: center;
font-family: monospace;
}
canvas {
border: 2px solid white;
background: #202020;
image-rendering: pixelated;
}
#ui {
margin-top: 10px;
}
</style>
</head>
<body>
<h1>レトロ風RPG</h1>
<canvas id="game" width="160" height="160"></canvas>
<div id="ui">
<p id="status">HP: 10</p>
<p id="log"></p>
</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("status");
const logEl = document.getElementById("log");
const tileSize = 16;
const map = [
[0, 0, 1, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 1, 0, 1, 1, 0, 0, 1, 0],
[1, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[1, 1, 1, 1, 1, 0, 1, 1, 0, 1],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 1, 1, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
];
const player = {
x: 0,
y: 0,
hp: 10,
color: "#ff0000"
};
const enemies = [
{ x: 4, y: 2, hp: 5, alive: true },
{ x: 8, y: 7, hp: 7, alive: true },
];
function drawMap() {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[0].length; x++) {
ctx.fillStyle = map[y][x] === 1 ? "#444" : "#88cc88";
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
}
}
function drawPlayer() {
ctx.fillStyle = player.color;
ctx.fillRect(player.x * tileSize, player.y * tileSize, tileSize, tileSize);
}
function drawEnemies() {
ctx.fillStyle = "#ffcc00";
enemies.forEach(enemy => {
if (enemy.alive) {
ctx.fillRect(enemy.x * tileSize, enemy.y * tileSize, tileSize, tileSize);
}
});
}
function canMove(x, y) {
return map[y] && map[y][x] === 0;
}
function updateUI() {
statusEl.textContent = `HP: ${player.hp}`;
}
function showLog(text) {
logEl.textContent = text;
}
function battle(enemy) {
showLog("戦闘開始!");
const battleInterval = setInterval(() => {
// プレイヤーの攻撃
let playerDmg = Math.floor(Math.random() * 3) + 1;
enemy.hp -= playerDmg;
showLog(`あなたの攻撃! 敵に${playerDmg}ダメージ!`);
if (enemy.hp <= 0) {
showLog("敵を倒した!");
enemy.alive = false;
clearInterval(battleInterval);
gameLoop();
return;
}
// 敵の攻撃
let enemyDmg = Math.floor(Math.random() * 3) + 1;
player.hp -= enemyDmg;
updateUI();
showLog(`敵の反撃! あなたは${enemyDmg}ダメージを受けた!`);
if (player.hp <= 0) {
showLog("あなたは倒れた… GAME OVER");
clearInterval(battleInterval);
document.removeEventListener("keydown", handleKey);
}
}, 1000);
}
function checkEnemy(x, y) {
for (let enemy of enemies) {
if (enemy.x === x && enemy.y === y && enemy.alive) {
battle(enemy);
return true;
}
}
return false;
}
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawMap();
drawEnemies();
drawPlayer();
updateUI();
}
function handleKey(e) {
let nx = player.x;
let ny = player.y;
if (e.key === "ArrowUp") ny--;
if (e.key === "ArrowDown") ny++;
if (e.key === "ArrowLeft") nx--;
if (e.key === "ArrowRight") nx++;
if (canMove(nx, ny)) {
player.x = nx;
player.y = ny;
if (!checkEnemy(nx, ny)) {
showLog(""); // 戦闘中じゃないならログを消す
}
}
gameLoop();
}
document.addEventListener("keydown", handleKey);
gameLoop();
</script>
</body>
</html>
出会い系サイト
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>出会い広場 - マッチング</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Arial', sans-serif;
background: #f2f2f2;
margin: 0;
padding: 0;
}
header {
background-color: #ff4d6d;
color: white;
padding: 20px;
text-align: center;
font-size: 24px;
}
.container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
label {
display: block;
margin-top: 10px;
}
input, textarea, select {
width: 100%;
padding: 10px;
margin-top: 5px;
border-radius: 4px;
border: 1px solid #ccc;
}
button {
margin-top: 15px;
background: #ff4d6d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.user-card {
background: #fff0f3;
padding: 10px;
margin-bottom: 10px;
border-radius: 6px;
display: flex;
align-items: center;
}
.user-card img {
width: 80px;
height: 80px;
border-radius: 50%;
margin-right: 15px;
object-fit: cover;
}
@media (max-width: 600px) {
.user-card {
flex-direction: column;
align-items: flex-start;
}
.user-card img {
margin-bottom: 10px;
}
}
</style>
</head>
<body>
<header>出会い広場</header>
<div class="container">
<h2>プロフィール登録</h2>
<form id="profileForm">
<label>ニックネーム</label>
<input type="text" id="nickname" required>
<label>プロフィール画像URL</label>
<input type="url" id="avatar" placeholder="https://example.com/avatar.jpg">
<label>年齢</label>
<input type="number" id="age" required>
<label>性別</label>
<select id="gender">
<option value="男性">男性</option>
<option value="女性">女性</option>
<option value="その他">その他</option>
</select>
<label>自己紹介</label>
<textarea id="bio" rows="3" required></textarea>
<button type="submit">登録する</button>
</form>
</div>
<div class="container">
<h2>ユーザー一覧</h2>
<label>性別で絞り込む:</label>
<select id="filterGender">
<option value="すべて">すべて</option>
<option value="男性">男性</option>
<option value="女性">女性</option>
<option value="その他">その他</option>
</select>
<div id="userList" style="margin-top:20px;"></div>
</div>
<script>
const form = document.getElementById('profileForm');
const userList = document.getElementById('userList');
const filterGender = document.getElementById('filterGender');
let users = [];
form.addEventListener('submit', function(e) {
e.preventDefault();
const user = {
nickname: document.getElementById('nickname').value,
age: document.getElementById('age').value,
gender: document.getElementById('gender').value,
bio: document.getElementById('bio').value,
avatar: document.getElementById('avatar').value || 'https://via.placeholder.com/80'
};
users.push(user);
form.reset();
renderUsers();
});
filterGender.addEventListener('change', renderUsers);
function renderUsers() {
const filter = filterGender.value;
userList.innerHTML = '';
users
.filter(user => filter === 'すべて' || user.gender === filter)
.forEach(user => {
const card = document.createElement('div');
card.className = 'user-card';
card.innerHTML = `
<img src="${user.avatar}" alt="avatar">
<div>
<strong>${user.nickname}</strong>(${user.age}歳・${user.gender})<br>
<p>${user.bio}</p>
</div>
`;
userList.appendChild(card);
});
}
</script>
</body>
</html>