<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>目に悪い 七色デカツリー</title>
<style>
:root{
--bg:
#07070b;
--glow: 18px;
}
html,body{height:100%; margin:0; background:var(--bg); overflow:hidden; font-family:system-ui, sans-serif;}
canvas{display:block; width:100vw; height:100vh;}
.hint{
position:fixed; left:12px; bottom:10px; color:
#fff; opacity:.75;
font-size:12px; user-select:none; text-shadow:0 0 10px #000;
}
.hint kbd{background:rgba(255,255,255,.12); border:1px solid rgba(255,255,255,.25); padding:2px 6px; border-radius:6px;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="hint">
<kbd>Space</kbd>点滅ON/OFF / <kbd>↑↓</kbd>発光量 / <kbd>R</kbd>リセット
</div>
<script>
(() => {
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d", { alpha: false });
// 七色固定(はっきり7色)
const RAINBOW = [
"
#ff2b2b", // 赤
"
#ff8a1f", // 橙
"
#ffe600", // 黄
"
#00ff66", // 緑
"
#00c8ff", // 水
"
#2b58ff", // 青
"
#b400ff" // 紫
];
let W=0,H=0, dpr=1;
function resize(){
dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
W = Math.floor(innerWidth * dpr);
H = Math.floor(innerHeight * dpr);
canvas.width = W; canvas.height = H;
}
addEventListener("resize", resize);
resize();
// 目に悪い制御
let strobe = true;
let glowBoost = 1.0;
addEventListener("keydown", (e) => {
if(e.code === "Space"){ strobe = !strobe; }
if(e.key === "ArrowUp"){ glowBoost = Math.min(2.5, glowBoost 0.1); }
if(e.key === "ArrowDown"){ glowBoost = Math.max(0.4, glowBoost - 0.1); }
if(e.key.toLowerCase() === "r"){ seed = (Math.random()*1e9)|0; buildOrnaments(); }
});
// 擬似乱数(安定配置用)
let seed = (Math.random()*1e9)|0;
function rnd(){
// xorshift32
seed ^= seed << 13; seed |= 0;
seed ^= seed >>> 17; seed |= 0;
seed ^= seed << 5; seed |= 0;
return ((seed >>> 0) / 4294967296);
}
// ツリー形パラメータ
function treeProfile(yNorm){
// yNorm: 0(頂点)〜1(裾)
// 幅が下ほど広がる(滑らかな円錐)
const t = Math.pow(yNorm, 0.9);
return 0.12 0.52 * t; // 横幅係数
}
// オーナメント(位置を“ツリー内部”にばらまく)
let ornaments = [];
function buildOrnaments(){
ornaments.length = 0;
const count = 220; // でかい
for(let i=0;i<count;i ){
const y = Math.pow(rnd(), 1.2); // 下に多め
const half = treeProfile(y);
const x = (rnd()*2 - 1) * half * (0.85 rnd()*0.25);
const r = (0.004 rnd()*0.012); // 半径(正規化)
ornaments.push({x, y, r, phase: rnd()*Math.PI*2, band: (i%7)});
}
// ライト(輪郭沿いに並べてツリー形を強調)
const outline = 220;
for(let i=0;i<outline;i ){
const y = i/(outline-1);
const half = treeProfile(y);
const side = (i%2===0)? -1 : 1;
const x = side * half * (0.92 0.05*Math.sin(i*0.2));
const r = 0.006 0.004*rnd();
ornaments.push({x, y, r, phase: rnd()*Math.PI*2, band: (i%7), outline:true});
}
}
buildOrnaments();
function drawStar(cx, cy, r, t){
const spikes = 5;
const outer = r;
const inner = r*0.45;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(t*0.6);
ctx.beginPath();
for(let i=0;i<spikes*2;i ){
const ang = (Math.PI*i)/spikes;
const rad = (i%2===0)? outer : inner;
ctx.lineTo(Math.cos(ang)*rad, Math.sin(ang)*rad);
}
ctx.closePath();
ctx.restore();
}
function glowCircle(x,y,r,color,alpha,blur){
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = blur;
ctx.beginPath();
ctx.arc(x,y,r,0,Math.PI*2);
ctx.fill();
ctx.restore();
}
function drawScanlines(time){
// スキャンライン(目に悪い)
ctx.save();
ctx.globalAlpha = 0.10;
ctx.fillStyle = "#000";
const step = Math.max(4, Math.floor(8*dpr));
for(let y=0; y<H; y =step){
const wobble = Math.floor((Math.sin(time*0.004 y*0.02) 1)*0.5*2);
ctx.fillRect(0, y wobble, W, Math.floor(step/2));
}
ctx.restore();
}
function frame(time){
// 背景フラッシュ(七色)
const bgIdx = Math.floor((time*0.004) % 7);
const bgPulse = strobe ? (0.18 0.22*Math.sin(time*0.02)) : 0.10;
ctx.fillStyle = "
#07070b";
ctx.fillRect(0,0,W,H);
// 薄い虹フラッシュ
ctx.save();
ctx.globalAlpha = Math.max(0, bgPulse);
ctx.fillStyle = RAINBOW[bgIdx];
ctx.fillRect(0,0,W,H);
ctx.restore();
const cx = W*0.5;
const topY = H*0.12;
const baseY = H*0.88;
// ツリー本体(段々の円 = “葉”が見えるように)
const layers = 18;
for(let i=0;i<layers;i ){
const yNorm = i/(layers-1);
const y = topY (baseY-topY)*yNorm*0.92;
const half = treeProfile(yNorm);
const radius = (half * W*0.55) * (0.65 0.02*Math.sin(time*0.01 i));
const height = (H*0.02 yNorm*H*0.02);
// 葉っぱの色も虹に寄せる
const col = RAINBOW[(i Math.floor(time*0.01)) % 7];
ctx.save();
ctx.globalAlpha = 0.30;
ctx.fillStyle = col;
ctx.shadowColor = col;
ctx.shadowBlur = 14*glowBoost*dpr;
ctx.beginPath();
ctx.ellipse(cx, y, radius, height, 0, 0, Math.PI*2);
ctx.fill();
ctx.restore();
// 濃い中心
ctx.save();
ctx.globalAlpha = 0.18;
ctx.fillStyle = "
#00ff66";
ctx.beginPath();
ctx.ellipse(cx, y, radius*0.72, height*0.75, 0, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
// 幹
const trunkW = W*0.07;
const trunkH = H*0.12;
ctx.save();
ctx.globalAlpha = 0.85;
ctx.fillStyle = "
#5b2a12";
ctx.shadowColor = "
#ffcc66";
ctx.shadowBlur = 10*glowBoost*dpr;
ctx.fillRect(cx - trunkW/2, baseY - trunkH*0.3, trunkW, trunkH);
ctx.restore();
// 星(頂点)
const starX = cx;
const starY = topY - H*0.02;
const starR = Math.min(W,H)*0.04;
// 星のグロー
const starColor = RAINBOW[Math.floor((time*0.02)%7)];
drawStar(starX, starY, starR, time*0.01);
ctx.save();
ctx.globalAlpha = strobe ? (0.75 0.25*Math.sin(time*0.05)) : 0.75;
ctx.fillStyle = "
#fff7b0";
ctx.shadowColor = starColor;
ctx.shadowBlur = 45*glowBoost*dpr;
ctx.fill();
ctx.restore();
// オーナメント&ライト(七色循環+点滅)
const treeWidth = W*0.55;
const treeHeight = (baseY-topY)*0.92;
for(let i=0;i<ornaments.length;i ){
const o = ornaments[i];
const x = cx o.x*treeWidth;
const y = topY o.y*treeHeight;
// 7色を“明確に”切り替えつつ、少し時間でずらす
const idx = (
o.band Math.floor((time*0.03 o.phase)*1.2)) % 7;
const col = RAINBOW[idx];
const blink = strobe ? (0.55 0.45*Math.sin(time*0.06 o.phase*3)) : 0.9;
const baseR = o.r*Math.min(W,H);
// 輪郭ライトはより強く
const intensity = o.outline ? 1.25 : 1.0;
const blur = (o.outline ? 42 : 28) * glowBoost * dpr;
// 外側グロー
glowCircle(x,y, baseR*1.35, col, 0.45*blink*intensity, blur);
// 本体
glowCircle(x,y, baseR*0.85, "
#ffffff", 0.20*blink*intensity, 10*glowBoost*dpr);
glowCircle(x,y, baseR*0.70, col, 0.95*blink*intensity, 18*glowBoost*dpr);
// ちょいハイライト
ctx.save();
ctx.globalAlpha = 0.35*blink;
ctx.fillStyle = "
#fff";
ctx.beginPath();
ctx.arc(x-baseR*0.25, y-baseR*0.25, baseR*0.22, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
// スノー(視界にノイズを足す)
const flakes = 260;
ctx.save();
ctx.globalAlpha = 0.28;
for(let i=0;i<flakes;i ){
const fx = ( (i*997 time*0.12) % W );
const fy = ( (i*571 time*0.55) % H );
const size = ( (i%7) 1 ) * 0.35 * dpr;
ctx.fillStyle = RAINBOW[(i Math.floor(time*0.01)) % 7];
ctx.fillRect(fx, fy, size, size);
}
ctx.restore();
// スキャンライン
drawScanlines(time);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>