HTML / CSS / JS
ビジュアル
2026/01/21
2026/1/16
動画やGIF画像を使わず、プログラム(シェーダー)で描画するため、画質が劣化せず、動作も非常に軽量なのが特徴です。サイバーパンクな雰囲気や、洗練された印象を与えたい時にぴったりのデザインです。
<div class="kumonosu-heart">
<canvas id="kumonosu-canvas"></canvas>
</div>
.kumonosu-heart {
background-color: #000;
margin: 0;
overflow: hidden;
background-repeat: no-repeat;
height: 100%;
}
// ===============================
// 1) Canvas / WebGL 初期化
// ===============================
const kumonosuCanvas = document.getElementById("kumonosu-canvas");
if (!kumonosuCanvas) throw new Error('Canvas "#kumonosu-canvas" not found.');
kumonosuCanvas.style.display = "block";
kumonosuCanvas.style.margin = "0";
kumonosuCanvas.style.backgroundColor = "#000";
kumonosuCanvas.style.borderRadius = "15px";
kumonosuCanvas.style.width = "100%";
kumonosuCanvas.style.height = "100vh"; // 必要に応じてCSS側で調整OK
const kumonosuGl = kumonosuCanvas.getContext("webgl", { antialias: true });
if (!kumonosuGl) throw new Error("WebGL is not supported in this browser.");
let kumonosuTime = 0;
// ===============================
// 2) Shader
// ===============================
const kumonosuVertexSource = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const kumonosuFragmentSource = `
precision highp float;
uniform float width;
uniform float height;
uniform float time;
#define POINT_COUNT 8
vec2 points[POINT_COUNT];
const float speed = -0.5;
const float len = 0.25;
float intensity = 1.3;
float radius = 0.008;
float sdBezier(vec2 pos, vec2 A, vec2 B, vec2 C){
vec2 a = B - A;
vec2 b = A - 2.0*B + C;
vec2 c = a * 2.0;
vec2 d = A - pos;
float kk = 1.0 / dot(b,b);
float kx = kk * dot(a,b);
float ky = kk * (2.0*dot(a,a)+dot(d,b)) / 3.0;
float kz = kk * dot(d,a);
float res = 0.0;
float p = ky - kx*kx;
float p3 = p*p*p;
float q = kx*(2.0*kx*kx - 3.0*ky) + kz;
float h = q*q + 4.0*p3;
if(h >= 0.0){
h = sqrt(h);
vec2 x = (vec2(h, -h) - q) / 2.0;
vec2 uv = sign(x)*pow(abs(x), vec2(1.0/3.0));
float t = uv.x + uv.y - kx;
t = clamp(t, 0.0, 1.0);
vec2 qos = d + (c + b*t)*t;
res = length(qos);
} else {
float z = sqrt(-p);
float v = acos(q/(p*z*2.0)) / 3.0;
float m = cos(v);
float n = sin(v)*1.732050808;
vec3 t = vec3(m + m, -n - m, n - m) * z - kx;
t = clamp(t, 0.0, 1.0);
vec2 qos = d + (c + b*t.x)*t.x;
float dis = dot(qos,qos);
res = dis;
qos = d + (c + b*t.y)*t.y;
dis = dot(qos,qos);
res = min(res,dis);
qos = d + (c + b*t.z)*t.z;
dis = dot(qos,qos);
res = min(res,dis);
res = sqrt(res);
}
return res;
}
vec2 getHeartPosition(float t){
return vec2(
16.0 * sin(t) * sin(t) * sin(t),
-(13.0 * cos(t) - 5.0 * cos(2.0*t) - 2.0 * cos(3.0*t) - cos(4.0*t))
);
}
float getGlow(float dist, float radius, float intensity){
return pow(radius/dist, intensity);
}
float getSegment(float t, vec2 pos, float offset, float scale){
for(int i = 0; i < POINT_COUNT; i++){
points[i] = getHeartPosition(offset + float(i)*len + fract(speed * t) * 6.28);
}
vec2 c = (points[0] + points[1]) / 2.0;
vec2 c_prev;
float dist = 10000.0;
for(int i = 0; i < POINT_COUNT-1; i++){
c_prev = c;
c = (points[i] + points[i+1]) / 2.0;
dist = min(dist, sdBezier(pos, scale * c_prev, scale * points[i], scale * c));
}
return max(0.0, dist);
}
void main(){
vec2 resolution = vec2(width, height);
vec2 uv = gl_FragCoord.xy / resolution.xy;
float ratio = resolution.x / resolution.y;
vec2 centre = vec2(0.5, 0.5);
vec2 pos = centre - uv;
pos.y /= ratio;
pos.y += 0.02;
float scale = 0.005;
float dist = getSegment(time, pos, 0.0, scale);
float glow = getGlow(dist, radius, intensity);
vec3 col = vec3(0.0);
col += 10.0 * vec3(smoothstep(0.003, 0.001, dist));
col += glow * vec3(1.0, 0.05, 0.3);
dist = getSegment(time, pos, 3.4, scale);
glow = getGlow(dist, radius, intensity);
col += 10.0 * vec3(smoothstep(0.003, 0.001, dist));
col += glow * vec3(0.1, 0.4, 1.0);
col = 1.0 - exp(-col);
col = pow(col, vec3(0.4545)); // ガンマ補正
gl_FragColor = vec4(col, 1.0);
}
`;
// ===============================
// 3) WebGL ユーティリティ
// ===============================
function kumonosuCompileShader(source, type) {
const shader = kumonosuGl.createShader(type);
kumonosuGl.shaderSource(shader, source);
kumonosuGl.compileShader(shader);
if (!kumonosuGl.getShaderParameter(shader, kumonosuGl.COMPILE_STATUS)) {
throw new Error(kumonosuGl.getShaderInfoLog(shader) || "Shader compile error.");
}
return shader;
}
function kumonosuGetAttrib(program, name) {
const loc = kumonosuGl.getAttribLocation(program, name);
if (loc === -1) throw new Error(`Attrib not found: ${name}`);
return loc;
}
function kumonosuGetUniform(program, name) {
const loc = kumonosuGl.getUniformLocation(program, name);
if (!loc) throw new Error(`Uniform not found: ${name}`);
return loc;
}
// ===============================
// 4) Program / Buffer セットアップ
// ===============================
const kumonosuProgram = kumonosuGl.createProgram();
kumonosuGl.attachShader(kumonosuProgram, kumonosuCompileShader(kumonosuVertexSource, kumonosuGl.VERTEX_SHADER));
kumonosuGl.attachShader(kumonosuProgram, kumonosuCompileShader(kumonosuFragmentSource, kumonosuGl.FRAGMENT_SHADER));
kumonosuGl.linkProgram(kumonosuProgram);
kumonosuGl.useProgram(kumonosuProgram);
// 全画面用の板ポリ(-1〜1)
const kumonosuVertexData = new Float32Array([-1, 1, -1, -1, 1, 1, 1, -1]);
const kumonosuBuffer = kumonosuGl.createBuffer();
kumonosuGl.bindBuffer(kumonosuGl.ARRAY_BUFFER, kumonosuBuffer);
kumonosuGl.bufferData(kumonosuGl.ARRAY_BUFFER, kumonosuVertexData, kumonosuGl.STATIC_DRAW);
const kumonosuPosition = kumonosuGetAttrib(kumonosuProgram, "position");
kumonosuGl.enableVertexAttribArray(kumonosuPosition);
kumonosuGl.vertexAttribPointer(kumonosuPosition, 2, kumonosuGl.FLOAT, false, 8, 0);
// uniform
const kumonosuTimeHandle = kumonosuGetUniform(kumonosuProgram, "time");
const kumonosuWidthHandle = kumonosuGetUniform(kumonosuProgram, "width");
const kumonosuHeightHandle = kumonosuGetUniform(kumonosuProgram, "height");
// ===============================
// 5) リサイズ(表示サイズに追従)
// ===============================
function kumonosuResize() {
const rect = kumonosuCanvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const displayW = Math.max(1, Math.floor(rect.width));
const displayH = Math.max(1, Math.floor(rect.height));
const internalW = Math.max(1, Math.floor(displayW * dpr));
const internalH = Math.max(1, Math.floor(displayH * dpr));
if (kumonosuCanvas.width !== internalW || kumonosuCanvas.height !== internalH) {
kumonosuCanvas.width = internalW;
kumonosuCanvas.height = internalH;
kumonosuGl.viewport(0, 0, internalW, internalH);
kumonosuGl.uniform1f(kumonosuWidthHandle, internalW);
kumonosuGl.uniform1f(kumonosuHeightHandle, internalH);
}
}
kumonosuResize();
window.addEventListener("resize", kumonosuResize);
// 親要素のサイズ変化(レイアウト変更)にも追従
if ("ResizeObserver" in window) {
const ro = new ResizeObserver(kumonosuResize);
ro.observe(kumonosuCanvas);
}
// ===============================
// 6) アニメーションループ
// ===============================
let kumonosuLast = performance.now();
function kumonosuDraw(now) {
const dt = (now - kumonosuLast) / 1000;
kumonosuLast = now;
kumonosuTime += dt;
kumonosuGl.uniform1f(kumonosuTimeHandle, kumonosuTime);
kumonosuGl.drawArrays(kumonosuGl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(kumonosuDraw);
}
requestAnimationFrame(kumonosuDraw);
このアニメーションは、単なる画像表示ではなく、WebGL(GLSLシェーダー)を使用したプロシージャル(数式ベース)な描画であることが最大の特徴です。
Webサイトにこのアニメーションを導入することで、以下の指標やユーザー体験にポジティブな影響を与える可能性があります。
SEOとパフォーマンスを維持するために、以下の点に注意が必要です。
コード内の変数を少し調整するだけで、サイトの雰囲気に合わせた変更が可能です。