Beating heart
하트 곡선 (x²+y²-1)³ = x²y³입니다. 부호로 안쪽을 채우고 fwidth로 가장자리를 다듬었으며, 뾰족하게 만든 사인파 박동에 맞춰 뛰고 박자에 맞춰 빛납니다.
// ═══ typeshade example — beating heart (implicit curve) ═══//// The sextic heart curve (x² + y² − 1)³ − x²y³ = 0, filled where the implicit// function goes negative, anti-aliased with `fwidth` on the field itself, and// scaled by a sharpened-sine heartbeat. A different shading recipe from the// SDF examples: implicit-function sign fill + field-driven inner gradient +// exponential outer glow synced to the beat. WGSL (WebGPU) + GLSL ES 3.00.
import { fn, module, f32, vec3, vec4, sin, abs, pow, max, mix, exp, clamp, smoothstep, fwidth, Let, f32T,} from '../src/index.js';import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.js';import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ beat: f32T });
const fs = fn( 'fs', { vo: VsOut }, ({ vo }) => { const t = U.field.time; const res = U.field.resolution; const p = screenCoords(vo.uv, res); // heartbeat: |sin|⁸ sharpens the sine into a thump per half-period const beat = Let(pow(abs(sin(t.mul(3.14159).mul(U.field.beat))), 8).mul(0.12)); const s = Let(f32(0.72).add(beat)); const qx = Let(p.x.mul(1.3).div(s)); const qy = Let(p.y.add(0.08).mul(1.3).div(s)); // the sextic heart: (x² + y² − 1)³ − x²·y³ const xx = Let(qx.mul(qx)); const k = Let(xx.add(qy.mul(qy)).sub(1)); const f = Let(k.mul(k).mul(k).sub(xx.mul(qy).mul(qy).mul(qy))); // fill where f < 0, anti-aliased on the field's own screen gradient const aa = Let(fwidth(f).mul(1.6)); const fill = f32(1).sub(smoothstep(aa.neg(), aa, f)); // inside: brighter toward the centre (f is most negative there) const heart = mix(vec3(0.55, 0.03, 0.1), vec3(0.95, 0.15, 0.25), clamp(f.neg(), 0, 1)); // outside: a soft glow that swells with the beat const glow = exp(max(f, 0).mul(2.2).neg()).mul(beat.mul(3).add(0.35)); const bg = vec3(0.05, 0.04, 0.07).mul(f32(1).sub(vo.uv.y.mul(0.35))); const col = mix(bg.add(vec3(0.7, 0.08, 0.16).mul(glow)), heart, fill); return vec4(col, 1); }, { stage: 'fragment', retAttr: '@location(0)' },);
const heartModule = module({ structs: [U.struct, VsOut.decl], bindings: [U.binding], funcs: [vs, fs],});
export const heart: ShaderExample = { id: 'heart', title: 'Beating heart', blurb: 'The sextic heart curve (x²+y²−1)³ = x²y³, sign-filled and fwidth-anti-aliased, thumping to a sharpened-sine heartbeat with a beat-synced glow. Beat rate is live.', category: 'generic', file: 'heart.ts', module: heartModule, renderable: true, controls: { time: { kind: 'time' }, resolution: { kind: 'resolution' }, beat: { kind: 'slider', label: 'Beat rate', min: 0.2, max: 2, step: 0.1, value: 1 }, },};struct Uniforms { time: f32, resolution: vec2<f32>, beat: f32,}
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(0) @binding(0) var<uniform> U: Uniforms;
@vertexfn vs(@builtin(vertex_index) vi: u32) -> VsOut { let _cse0 = ((f32((vi & 1u)) * 4.0) - 1.0); let _cse1 = ((f32((vi >> 1u)) * 4.0) - 1.0); return VsOut(vec4<f32>(_cse0, _cse1, 0.0, 1.0), vec2<f32>(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));}
@fragmentfn fs(vo: VsOut) -> @location(0) vec4<f32> { let _v0 = (pow(abs(sin(((U.time * 3.14159) * U.beat))), 8.0) * 0.12); let _v1 = (0.72 + _v0); let _cse0 = vec2<f32>((((vo.uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((vo.uv.y * 2.0) - 1.0)); let _v2 = ((_cse0.x * 1.3) / _v1); let _v3 = (((_cse0.y + 0.08) * 1.3) / _v1); let _v4 = (_v2 * _v2); let _v5 = ((_v4 + (_v3 * _v3)) - 1.0); let _v6 = (((_v5 * _v5) * _v5) - (((_v4 * _v3) * _v3) * _v3)); let _v7 = (fwidth(_v6) * 1.6); return vec4<f32>(mix(((vec3<f32>(0.05, 0.04, 0.07) * (1.0 - (vo.uv.y * 0.35))) + (vec3<f32>(0.7, 0.08, 0.16) * (exp((-(max(_v6, 0.0) * 2.2))) * ((_v0 * 3.0) + 0.35)))), mix(vec3<f32>(0.55, 0.03, 0.1), vec3<f32>(0.95, 0.15, 0.25), clamp((-_v6), 0.0, 1.0)), (1.0 - smoothstep((-_v7), _v7, _v6))), 1.0);}#version 300 esprecision highp float;precision highp int;
out vec2 uv;
void main() { uint vi = uint(gl_VertexID); float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0); float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0); gl_Position = vec4(_cse0, _cse1, 0.0, 1.0); uv = vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5));}#version 300 esprecision highp float;precision highp int;
layout(std140) uniform Uniforms { float time; vec2 resolution; float beat;} U;in vec2 uv;layout(location = 0) out vec4 _ret;
void main() { float _v0 = (pow(abs(sin(((U.time * 3.14159) * U.beat))), 8.0) * 0.12); float _v1 = (0.72 + _v0); vec2 _cse0 = vec2((((uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((uv.y * 2.0) - 1.0)); float _v2 = ((_cse0.x * 1.3) / _v1); float _v3 = (((_cse0.y + 0.08) * 1.3) / _v1); float _v4 = (_v2 * _v2); float _v5 = ((_v4 + (_v3 * _v3)) - 1.0); float _v6 = (((_v5 * _v5) * _v5) - (((_v4 * _v3) * _v3) * _v3)); float _v7 = (fwidth(_v6) * 1.6); _ret = vec4(mix(((vec3(0.05, 0.04, 0.07) * (1.0 - (uv.y * 0.35))) + (vec3(0.7, 0.08, 0.16) * (exp((-(max(_v6, 0.0) * 2.2))) * ((_v0 * 3.0) + 0.35)))), mix(vec3(0.55, 0.03, 0.1), vec3(0.95, 0.15, 0.25), clamp((-_v6), 0.0, 1.0)), (1.0 - smoothstep((-_v7), _v7, _v6))), 1.0);}Beating heart. 빌드할 때 그린 화면입니다.
WGSL과 GLSL 탭은 커밋 c66579bf의 컴파일러가 직접 낸 출력입니다. 컴파일러의 출력 검사가 구워 둔 골든 파일에서 그대로 읽어 왔습니다(emit-goldens.test.ts).
이 예제는 fn() 빌더 API로 작성해서 Playground의 편집기가 받지 않습니다.