@omnedia/ngx-orb
Version:
An Angular standalone component for an animated, interactive 3D orb with procedural GLSL shaders (OGL/WebGL).
338 lines (311 loc) • 13.9 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, PLATFORM_ID, Input, ViewChild, Inject, ChangeDetectionStrategy, Component } from '@angular/core';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import { Renderer, Triangle, Program, Vec3, Mesh } from 'ogl';
class NgxOrbComponent {
platformId;
containerRef;
canvasWrapperRef;
set hue(v) {
this.hueSignal.set(v);
if (this.initialized)
this.updateUniform('hue', v);
}
set hoverIntensity(v) {
this.hoverIntensitySignal.set(v);
if (this.initialized)
this.updateUniform('hoverIntensity', v);
}
set rotateOnHover(v) {
this.rotateOnHoverSignal.set(v);
}
set forceHoverState(v) {
this.forceHoverStateSignal.set(v);
}
hueSignal = signal(0);
hoverIntensitySignal = signal(0.2);
rotateOnHoverSignal = signal(true);
forceHoverStateSignal = signal(false);
renderer;
gl;
program;
mesh;
rafId;
initialized = false;
lastTime = 0;
targetHover = 0;
currentRot = 0;
running = true;
intersectionObserver;
constructor(platformId) {
this.platformId = platformId;
}
ngAfterViewInit() {
if (!isPlatformBrowser(this.platformId))
return;
this.setupOGL();
this.observeInView();
}
ngOnDestroy() {
this.running = false;
if (this.rafId)
cancelAnimationFrame(this.rafId);
if (this.intersectionObserver)
this.intersectionObserver.disconnect();
if (this.gl?.canvas && this.canvasWrapperRef.nativeElement.contains(this.gl.canvas)) {
this.canvasWrapperRef.nativeElement.removeChild(this.gl.canvas);
}
this.gl?.getExtension('WEBGL_lose_context')?.loseContext();
window.removeEventListener('resize', this.resizeHandler);
this.containerRef.nativeElement.removeEventListener('mousemove', this.handleMouseMove);
this.containerRef.nativeElement.removeEventListener('mouseleave', this.handleMouseLeave);
}
observeInView() {
this.intersectionObserver = new IntersectionObserver(([entry]) => {
this.running = entry.isIntersecting;
if (this.running)
this.rafId = requestAnimationFrame(this.update);
});
this.intersectionObserver.observe(this.containerRef.nativeElement);
}
setupOGL() {
const vert = `precision highp float; attribute vec2 position; attribute vec2 uv; varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 0.0, 1.0); }`;
// (Use your full fragment shader code here)
const frag = `
precision highp float;
uniform float iTime;
uniform vec3 iResolution;
uniform float hue;
uniform float hover;
uniform float rot;
uniform float hoverIntensity;
varying vec2 vUv;
vec3 rgb2yiq(vec3 c) {
float y = dot(c, vec3(0.299, 0.587, 0.114));
float i = dot(c, vec3(0.596, -0.274, -0.322));
float q = dot(c, vec3(0.211, -0.523, 0.312));
return vec3(y, i, q);
}
vec3 yiq2rgb(vec3 c) {
float r = c.x + 0.956 * c.y + 0.621 * c.z;
float g = c.x - 0.272 * c.y - 0.647 * c.z;
float b = c.x - 1.106 * c.y + 1.703 * c.z;
return vec3(r, g, b);
}
vec3 adjustHue(vec3 color, float hueDeg) {
float hueRad = hueDeg * 3.14159265 / 180.0;
vec3 yiq = rgb2yiq(color);
float cosA = cos(hueRad);
float sinA = sin(hueRad);
float i = yiq.y * cosA - yiq.z * sinA;
float q = yiq.y * sinA + yiq.z * cosA;
yiq.y = i;
yiq.z = q;
return yiq2rgb(yiq);
}
vec3 hash33(vec3 p3) {
p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
p3 += dot(p3, p3.yxz + 19.19);
return -1.0 + 2.0 * fract(vec3(
p3.x + p3.y,
p3.x + p3.z,
p3.y + p3.z
) * p3.zyx);
}
float snoise3(vec3 p) {
const float K1 = 0.333333333;
const float K2 = 0.166666667;
vec3 i = floor(p + (p.x + p.y + p.z) * K1);
vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
vec3 e = step(vec3(0.0), d0 - d0.yzx);
vec3 i1 = e * (1.0 - e.zxy);
vec3 i2 = 1.0 - e.zxy * (1.0 - e);
vec3 d1 = d0 - (i1 - K2);
vec3 d2 = d0 - (i2 - K1);
vec3 d3 = d0 - 0.5;
vec4 h = max(0.6 - vec4(
dot(d0, d0),
dot(d1, d1),
dot(d2, d2),
dot(d3, d3)
), 0.0);
vec4 n = h * h * h * h * vec4(
dot(d0, hash33(i)),
dot(d1, hash33(i + i1)),
dot(d2, hash33(i + i2)),
dot(d3, hash33(i + 1.0))
);
return dot(vec4(31.316), n);
}
vec4 extractAlpha(vec3 colorIn) {
float a = max(max(colorIn.r, colorIn.g), colorIn.b);
return vec4(colorIn.rgb / (a + 1e-5), a);
}
const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);
const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);
const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);
const float innerRadius = 0.6;
const float noiseScale = 0.65;
float light1(float intensity, float attenuation, float dist) {
return intensity / (1.0 + dist * attenuation);
}
float light2(float intensity, float attenuation, float dist) {
return intensity / (1.0 + dist * dist * attenuation);
}
vec4 draw(vec2 uv) {
vec3 color1 = adjustHue(baseColor1, hue);
vec3 color2 = adjustHue(baseColor2, hue);
vec3 color3 = adjustHue(baseColor3, hue);
float ang = atan(uv.y, uv.x);
float len = length(uv);
float invLen = len > 0.0 ? 1.0 / len : 0.0;
float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;
float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);
float d0 = distance(uv, (r0 * invLen) * uv);
float v0 = light1(1.0, 10.0, d0);
v0 *= smoothstep(r0 * 1.05, r0, len);
float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;
float a = iTime * -1.0;
vec2 pos = vec2(cos(a), sin(a)) * r0;
float d = distance(uv, pos);
float v1 = light2(1.5, 5.0, d);
v1 *= light1(1.0, 50.0, d0);
float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);
vec3 col = mix(color1, color2, cl);
col = mix(color3, col, v0);
col = (col + v1) * v2 * v3;
col = clamp(col, 0.0, 1.0);
return extractAlpha(col);
}
vec4 mainImage(vec2 fragCoord) {
vec2 center = iResolution.xy * 0.5;
float size = min(iResolution.x, iResolution.y);
vec2 uv = (fragCoord - center) / size * 2.0;
float angle = rot;
float s = sin(angle);
float c = cos(angle);
uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);
uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);
return draw(uv);
}
void main() {
vec2 fragCoord = vUv * iResolution.xy;
vec4 col = mainImage(fragCoord);
gl_FragColor = vec4(col.rgb * col.a, col.a);
}
`;
this.renderer = new Renderer({ alpha: true, premultipliedAlpha: false });
this.gl = this.renderer.gl;
if (!this.gl) {
return;
}
this.gl.clearColor(0, 0, 0, 0);
this.canvasWrapperRef.nativeElement.appendChild(this.gl.canvas);
const geometry = new Triangle(this.gl);
this.program = new Program(this.gl, {
vertex: vert,
fragment: frag,
uniforms: {
iTime: { value: 0 },
iResolution: { value: new Vec3(this.gl.canvas.width, this.gl.canvas.height, this.gl.canvas.width / this.gl.canvas.height) },
hue: { value: this.hueSignal() },
hover: { value: 0 },
rot: { value: 0 },
hoverIntensity: { value: this.hoverIntensitySignal() },
},
});
this.mesh = new Mesh(this.gl, { geometry, program: this.program });
window.addEventListener('resize', this.resizeHandler);
this.containerRef.nativeElement.addEventListener('mousemove', this.handleMouseMove);
this.containerRef.nativeElement.addEventListener('mouseleave', this.handleMouseLeave);
this.resizeHandler();
this.initialized = true;
this.rafId = requestAnimationFrame(this.update);
}
resizeHandler = () => {
if (!this.canvasWrapperRef.nativeElement)
return;
const dpr = window.devicePixelRatio || 1;
const width = this.canvasWrapperRef.nativeElement.clientWidth;
const height = this.canvasWrapperRef.nativeElement.clientHeight;
this.renderer.setSize(width * dpr, height * dpr);
this.gl.canvas.style.width = width + "px";
this.gl.canvas.style.height = height + "px";
this.program.uniforms['iResolution'].value.set(this.gl.canvas.width, this.gl.canvas.height, this.gl.canvas.width / this.gl.canvas.height);
};
handleMouseMove = (e) => {
const rect = this.canvasWrapperRef.nativeElement.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const width = rect.width;
const height = rect.height;
const size = Math.min(width, height);
const centerX = width / 2;
const centerY = height / 2;
const uvX = ((x - centerX) / size) * 2.0;
const uvY = ((y - centerY) / size) * 2.0;
this.targetHover = Math.sqrt(uvX * uvX + uvY * uvY) < 0.8 ? 1 : 0;
};
handleMouseLeave = () => {
this.targetHover = 0;
};
updateUniform(key, value) {
if (this.program && this.program.uniforms && key in this.program.uniforms) {
this.program.uniforms[key].value = value;
}
}
update = (t) => {
if (!this.running || !this.program)
return;
const dt = (t - this.lastTime) * 0.001;
this.lastTime = t;
const hue = this.hueSignal();
const hoverIntensity = this.hoverIntensitySignal();
const rotateOnHover = this.rotateOnHoverSignal();
const forceHoverState = this.forceHoverStateSignal();
this.program.uniforms['iTime'].value = t * 0.001;
this.program.uniforms['hue'].value = hue;
this.program.uniforms['hoverIntensity'].value = hoverIntensity;
const effectiveHover = forceHoverState ? 1 : this.targetHover;
this.program.uniforms['hover'].value += (effectiveHover - this.program.uniforms['hover'].value) * 0.1;
if (rotateOnHover && effectiveHover > 0.5) {
this.currentRot += dt * 0.3;
}
this.program.uniforms['rot'].value = this.currentRot;
this.renderer.render({ scene: this.mesh });
this.rafId = requestAnimationFrame(this.update);
};
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxOrbComponent, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.3", type: NgxOrbComponent, isStandalone: true, selector: "om-orb", inputs: { hue: "hue", hoverIntensity: "hoverIntensity", rotateOnHover: "rotateOnHover", forceHoverState: "forceHoverState" }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["OmOrb"], descendants: true }, { propertyName: "canvasWrapperRef", first: true, predicate: ["OmOrbCanvas"], descendants: true }], ngImport: i0, template: "<div #OmOrb class=\"om-orb\">\n <div class=\"om-orb-canvas\" #OmOrbCanvas></div>\n <div class=\"om-orb-content\">\n <ng-content></ng-content>\n </div>\n</div>\n", styles: [".om-orb,.om-orb .om-orb-content{position:relative;width:100%;height:100%}.om-orb .om-orb-canvas{position:absolute;width:100%;height:100%;top:0;left:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxOrbComponent, decorators: [{
type: Component,
args: [{ selector: 'om-orb', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div #OmOrb class=\"om-orb\">\n <div class=\"om-orb-canvas\" #OmOrbCanvas></div>\n <div class=\"om-orb-content\">\n <ng-content></ng-content>\n </div>\n</div>\n", styles: [".om-orb,.om-orb .om-orb-content{position:relative;width:100%;height:100%}.om-orb .om-orb-canvas{position:absolute;width:100%;height:100%;top:0;left:0}\n"] }]
}], ctorParameters: () => [{ type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }], propDecorators: { containerRef: [{
type: ViewChild,
args: ['OmOrb']
}], canvasWrapperRef: [{
type: ViewChild,
args: ['OmOrbCanvas']
}], hue: [{
type: Input
}], hoverIntensity: [{
type: Input
}], rotateOnHover: [{
type: Input
}], forceHoverState: [{
type: Input
}] } });
/*
* Public API Surface of ngx-orb
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxOrbComponent };
//# sourceMappingURL=omnedia-ngx-orb.mjs.map