Skip to content

Instantly share code, notes, and snippets.

@c016smith
Created September 14, 2025 05:40
Show Gist options
  • Select an option

  • Save c016smith/ab111d6de815415e5a4ba0b5586f3882 to your computer and use it in GitHub Desktop.

Select an option

Save c016smith/ab111d6de815415e5a4ba0b5586f3882 to your computer and use it in GitHub Desktop.
WebGL Dimensional Portal with Three.js
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Cosmic Dimensional Portal</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Arial', sans-serif; }
#portal-container { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; }
.dg.ac { z-index: 1000 !important; }
#loading-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(45deg, #000, #1a0033);
display: flex; justify-content: center; align-items: center;
z-index: 9999; transition: opacity 1s ease-out;
}
.loader {
width: 150px; height: 150px; border: 44px solid transparent;
border-radius: 50%; border-top-color: #9b59b6; border-bottom-color: #3498db;
animation: spin 1.5s ease-in-out infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#info-panel {
position: fixed; bottom: 20px; left: 20px;
color: rgba(255, 255, 255, 0.8); font-size: 14px;
pointer-events: none; text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
z-index: 100; opacity: 0; transition: opacity 1s ease-in;
}
#control-panel {
position: fixed;
top: 20px;
right: 20px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 15px;
border: 1px solid rgba(155, 89, 182, 0.3);
backdrop-filter: blur(15px);
}
.control-row {
display: flex;
gap: 10px;
justify-content: center;
}
.control-btn {
background: rgba(155, 89, 182, 0.2);
border: 1px solid rgba(155, 89, 182, 0.4);
color: white;
width: 50px;
height: 50px;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.control-btn:hover {
background: rgba(155, 89, 182, 0.4);
border-color: rgba(255, 255, 255, 0.8);
transform: scale(1.1);
box-shadow: 0 0 20px rgba(155, 89, 182, 0.6);
}
.control-btn:active {
transform: scale(0.95);
}
.control-btn svg {
width: 24px;
height: 24px;
z-index: 2;
}
#portal-btn {
width: 100%;
height: 60px;
border-radius: 10px;
background: linear-gradient(45deg, rgba(155, 89, 182, 0.3), rgba(52, 152, 219, 0.3));
border-color: rgba(155, 89, 182, 0.6);
}
#portal-btn:hover {
background: linear-gradient(45deg, rgba(155, 89, 182, 0.5), rgba(52, 152, 219, 0.5));
box-shadow: 0 0 25px rgba(155, 89, 182, 0.7);
}
.btn-ripple {
position: absolute; border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
transform: scale(0); animation: ripple 0.6s linear;
pointer-events: none;
}
@keyframes ripple {
to { transform: scale(4); opacity: 0; }
}
#portal-indicator {
position: fixed; top: 20px; left: 20px; z-index: 100;
background: rgba(0, 0, 0, 0.8); border: 2px solid rgba(155, 89, 182, 0.5);
border-radius: 10px; padding: 15px; backdrop-filter: blur(10px);
color: rgba(255, 255, 255, 0.9); min-width: 200px;
opacity: 0; transition: all 0.3s ease;
}
#portal-indicator.active {
opacity: 1; border-color: rgba(52, 152, 219, 0.8);
box-shadow: 0 0 20px rgba(52, 152, 219, 0.3);
}
.portal-bar {
width: 100%; height: 8px; background: rgba(255, 255, 255, 0.2);
border-radius: 4px; margin-top: 8px; overflow: hidden;
}
.portal-fill {
height: 100%; background: linear-gradient(90deg, #9b59b6, #3498db);
border-radius: 4px; transition: width 0.3s ease;
box-shadow: 0 0 10px rgba(155, 89, 182, 0.5);
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
</head>
<div id="loading-overlay">
<div class="loader"></div>
</div>
<div id="portal-container"></div>
<div id="info-panel">
<p><strong>Dimensional Portal</strong></p>
<p>Mouse: Navigate | Double-click: Fullscreen</p>
</div>
<div id="portal-indicator">
<div>Portal Stability</div>
<div id="portal-status">Stabilizing</div>
<div class="portal-bar">
<div class="portal-fill" id="portal-fill" style="width: 100%"></div>
</div>
</div>
<div id="control-panel">
<button class="control-btn" id="portal-btn" title="Activate Portal">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M12 1v6m0 6v6"></path>
<path d="m21 12-6-3 6-3"></path>
<path d="m3 12 6 3-6 3"></path>
<path d="m21 12-6 3 6 3"></path>
<path d="m3 12 6-3-6-3"></path>
</svg>
</button>
<div class="control-row">
<button class="control-btn" id="reset-view" title="Reset Camera">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
<path d="M21 3v5h-5"></path>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
<path d="M3 21v-5h5"></path>
</svg>
</button>
<button class="control-btn" id="randomize" title="Shift Dimensions">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3a6.364 6.364 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
<path d="m7 14 3-3 3 3"></path>
<path d="M2.5 17a24.12 24.12 0 0 0 2 2 24.12 24.12 0 0 0 2-2"></path>
<path d="M22 12c0 6-4 6-6 0V9a4.5 4.5 0 0 0-4.5-4.5c-1.5 0-2.5 1.5-2.5 1.5s1-1.5 2.5-1.5A4.5 4.5 0 0 1 18 9v3Z"></path>
</svg>
</button>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
let portalEnergy = 100;
let portalCooldown = false;
function createRipple(button) {
const ripple = document.createElement('span');
ripple.classList.add('btn-ripple');
button.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
}
class CosmicPortalVisualization {
constructor() {
this.params = {
portalComplexity: 4,
crystalCount: 12,
primaryColor: '#9b59b6',
secondaryColor: '#3498db',
accentColor: '#e74c3c',
vortexColor: '#2ecc71',
rotationSpeed: 0.3,
bloomEnabled: true,
bloomStrength: 1.2,
bloomRadius: 0.7,
bloomThreshold: 0.2,
portalIntensity: 1.0,
dimensionShift: 0
};
this.meshes = [];
this.materials = [];
this.portalMaterials = [];
this.time = 0;
this.init();
this.createPortalScene();
this.setupGUI();
this.setupEventListeners();
this.animate();
setTimeout(() => {
const overlay = document.getElementById('loading-overlay');
overlay.style.opacity = '0';
setTimeout(() => {
overlay.style.display = 'none';
document.getElementById('info-panel').style.opacity = '1';
document.getElementById('portal-indicator').style.opacity = '1';
}, 300);
}, 100);
}
init() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0a0015);
this.scene.fog = new THREE.FogExp2(0x1a0033, 0.001);
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.position.set(0, 0, 15);
this.initialCameraPosition = this.camera.position.clone();
this.scene.add(new THREE.AmbientLight(0x330066, 0.2));
const mainLight = new THREE.DirectionalLight(0xffffff, 0.6);
mainLight.position.set(10, 10, 5);
this.scene.add(mainLight);
this.portalLights = [];
const lightColors = [this.params.primaryColor, this.params.secondaryColor, this.params.accentColor, this.params.vortexColor];
for (let i = 0; i < 6; i++) {
const light = new THREE.PointLight(new THREE.Color(lightColors[i % 4]), 0.8, 20);
this.scene.add(light);
this.portalLights.push(light);
}
this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.2;
document.getElementById('portal-container').appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.08;
this.controls.autoRotate = true;
this.controls.autoRotateSpeed = 0.5;
this.controls.minDistance = 8;
this.controls.maxDistance = 40;
this.composer = new EffectComposer(this.renderer);
this.composer.addPass(new RenderPass(this.scene, this.camera));
this.bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
this.params.bloomStrength, this.params.bloomRadius, this.params.bloomThreshold
);
this.composer.addPass(this.bloomPass);
this.fxaaPass = new ShaderPass(FXAAShader);
const pixelRatio = this.renderer.getPixelRatio();
this.fxaaPass.material.uniforms['resolution'].value.set(
1 / (window.innerWidth * pixelRatio), 1 / (window.innerHeight * pixelRatio)
);
this.composer.addPass(this.fxaaPass);
this.clock = new THREE.Clock();
}
createPortalScene() {
this.meshes.forEach(m => this.scene.remove(m));
this.materials.forEach(mat => mat?.dispose());
this.meshes = [];
this.materials = [];
this.portalMaterials = [];
this.createCosmicBackground();
this.createPortalCore();
this.createVortexRings();
this.createFloatingCrystals();
this.createDimensionalStreams();
this.createPortalFrame();
this.createEnergyParticles();
this.createSpaceDistortion();
}
activatePortal() {
if (portalCooldown || portalEnergy < 25) return;
portalCooldown = true;
portalEnergy = Math.max(0, portalEnergy - 25);
this.updatePortalUI();
this.portalMaterials.forEach(mat => {
if(mat.uniforms && mat.uniforms.pulseTime) {
mat.uniforms.pulseTime.value = this.time;
}
});
setTimeout(() => { portalCooldown = false; }, 1000);
}
addPortalShader(material) {
material.onBeforeCompile = (shader) => {
shader.uniforms.time = { value: 0 };
shader.uniforms.pulseTime = { value: -1000 };
shader.uniforms.portalSpeed = { value: 8.0 };
shader.uniforms.portalColor = { value: new THREE.Color(this.params.accentColor) };
shader.uniforms.dimensionShift = { value: 0 };
shader.vertexShader = `varying vec3 vWorldPosition;\n` + shader.vertexShader;
shader.fragmentShader = `
uniform float time;
uniform float pulseTime;
uniform float portalSpeed;
uniform vec3 portalColor;
uniform float dimensionShift;
varying vec3 vWorldPosition;\n` + shader.fragmentShader;
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
vWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz;
`
);
shader.fragmentShader = shader.fragmentShader.replace(
'#include <emissivemap_fragment>',
`#include <emissivemap_fragment>
float timeSincePortal = time - pulseTime;
if(timeSincePortal > 0.0 && timeSincePortal < 3.0) {
float portalRadius = timeSincePortal * portalSpeed;
float currentRadius = length(vWorldPosition);
float portalWidth = 1.5;
float portalEffect = smoothstep(portalRadius - portalWidth, portalRadius, currentRadius) -
smoothstep(portalRadius, portalRadius + portalWidth, currentRadius);
vec3 dimensionalColor = mix(portalColor, vec3(1.0, 0.5, 1.0), sin(dimensionShift * 3.14159) * 0.5 + 0.5);
totalEmissiveRadiance += dimensionalColor * portalEffect * 4.0;
}`
);
this.portalMaterials.push(shader);
};
}
createCosmicBackground() {
const count = 4000;
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const sizes = new Float32Array(count);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const radius = 80 + Math.random() * 50;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = radius * Math.cos(phi);
const temp = Math.random();
const color = new THREE.Color();
if (temp < 0.15) {
color.setHSL(0.8, 0.8, 0.9);
} else if (temp < 0.4) {
color.setHSL(0.6, 0.6, 0.8);
} else if (temp < 0.7) {
color.setHSL(0.1, 0.3, 0.9);
} else {
color.setHSL(0.3, 0.7, 0.6);
}
color.toArray(colors, i3);
sizes[i] = Math.random() * 0.5 + 0.1;
}
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const mat = new THREE.PointsMaterial({
size: 0.3, vertexColors: true, sizeAttenuation: true,
blending: THREE.AdditiveBlending, depthWrite: false, transparent: true
});
const stars = new THREE.Points(geo, mat);
this.scene.add(stars);
this.meshes.push(stars);
this.materials.push(mat);
}
createPortalCore() {
const geo = new THREE.SphereGeometry(0.8, 32, 32);
const mat = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
pulseTime: { value: -1000 },
dimensionShift: { value: 0 },
color1: { value: new THREE.Color(this.params.primaryColor) },
color2: { value: new THREE.Color(this.params.secondaryColor) },
color3: { value: new THREE.Color(this.params.accentColor) }
},
vertexShader: `
uniform float time;
uniform float dimensionShift;
varying vec3 vPos;
varying vec3 vNorm;
void main() {
vPos = position;
vNorm = normal;
float warp = sin(position.x * 10.0 + time * 3.0) * 0.1;
float shift = sin(dimensionShift * 6.28318) * 0.3;
vec3 p = position * (1.0 + warp + shift);
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform float pulseTime;
uniform float dimensionShift;
uniform vec3 color1;
uniform vec3 color2;
uniform vec3 color3;
varying vec3 vPos;
varying vec3 vNorm;
void main() {
float noise = sin(vPos.x * 20.0 + time * 4.0) * cos(vPos.z * 15.0 + time * 3.0);
vec3 baseColor = mix(color1, color2, 0.5 + 0.5 * sin(time * 2.0 + dimensionShift));
vec3 finalColor = mix(baseColor, color3, noise * 0.3);
float fresnel = pow(1.0 - abs(dot(vNorm, normalize(cameraPosition - vPos))), 3.0);
finalColor = mix(finalColor, vec3(1.0), fresnel * 0.5);
float timeSincePortal = time - pulseTime;
if(timeSincePortal > 0.0 && timeSincePortal < 1.0) {
float burst = 1.0 - timeSincePortal;
finalColor += vec3(1.0) * burst * 3.0;
}
gl_FragColor = vec4(finalColor, 0.9);
}
`,
transparent: true
});
this.portalMaterials.push(mat);
const mesh = new THREE.Mesh(geo, mat);
this.scene.add(mesh);
this.meshes.push(mesh);
}
createVortexRings() {
for (let ring = 0; ring < 5; ring++) {
const radius = 2 + ring * 0.8;
const geo = new THREE.TorusGeometry(radius, 0.05, 16, 64);
const mat = new THREE.MeshPhysicalMaterial({
color: new THREE.Color(this.getRingColor(ring)),
transparent: true, opacity: 0.7, metalness: 0.8, roughness: 0.2,
clearcoat: 0.8, clearcoatRoughness: 0.1,
emissive: new THREE.Color(this.getRingColor(ring)).multiplyScalar(0.2)
});
this.addPortalShader(mat);
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = Math.PI * 0.1 * ring;
mesh.rotation.z = Math.PI * 0.15 * ring;
this.scene.add(mesh);
this.meshes.push(mesh);
}
}
createFloatingCrystals() {
const crystalCount = this.params.crystalCount;
for (let i = 0; i < crystalCount; i++) {
const geo = new THREE.OctahedronGeometry(0.3 + Math.random() * 0.4, 1);
const mat = new THREE.MeshPhysicalMaterial({
color: new THREE.Color(this.getCrystalColor(i)),
transparent: true, opacity: 0.8, metalness: 0.9, roughness: 0.1,
clearcoat: 1.0, clearcoatRoughness: 0.0,
emissive: new THREE.Color(this.getCrystalColor(i)).multiplyScalar(0.3)
});
this.addPortalShader(mat);
const mesh = new THREE.Mesh(geo, mat);
const angle = (i / crystalCount) * Math.PI * 2;
const radius = 6 + Math.random() * 4;
mesh.position.set(
Math.cos(angle) * radius,
(Math.random() - 0.5) * 8,
Math.sin(angle) * radius
);
mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
this.scene.add(mesh);
this.meshes.push(mesh);
}
}
createDimensionalStreams() {
const streamCount = 8;
for (let i = 0; i < streamCount; i++) {
const points = [];
const segments = 120;
for (let j = 0; j <= segments; j++) {
const t = j / segments;
const angle = t * Math.PI * 12 + i * Math.PI * 0.25;
const radius = 3 + Math.sin(t * Math.PI * 6) * 1.5;
const height = (t - 0.5) * 15;
points.push(new THREE.Vector3(
Math.cos(angle) * radius,
height,
Math.sin(angle) * radius
));
}
const curve = new THREE.CatmullRomCurve3(points);
const geo = new THREE.TubeGeometry(curve, segments, 0.02, 8, false);
const mat = new THREE.MeshPhysicalMaterial({
color: new THREE.Color(this.getStreamColor(i)),
transparent: true, opacity: 0.6, metalness: 1.0, roughness: 0.0,
emissive: new THREE.Color(this.getStreamColor(i)).multiplyScalar(0.4)
});
this.addPortalShader(mat);
const stream = new THREE.Mesh(geo, mat);
this.scene.add(stream);
this.meshes.push(stream);
}
}
createPortalFrame() {
const frameGeo = new THREE.TorusGeometry(7, 0.2, 16, 64);
const frameMat = new THREE.MeshPhysicalMaterial({
color: new THREE.Color(this.params.primaryColor),
transparent: true, opacity: 0.4, metalness: 1.0, roughness: 0.1,
clearcoat: 1.0, clearcoatRoughness: 0.0,
emissive: new THREE.Color(this.params.primaryColor).multiplyScalar(0.5)
});
this.addPortalShader(frameMat);
const frame = new THREE.Mesh(frameGeo, frameMat);
this.scene.add(frame);
this.meshes.push(frame);
}
createEnergyParticles() {
const count = 1500;
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const r = 2 + Math.random() * 8;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i*3] = r * Math.sin(phi) * Math.cos(theta);
positions[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
positions[i*3+2] = r * Math.cos(phi);
velocities[i*3] = (Math.random() - 0.5) * 0.02;
velocities[i*3+1] = (Math.random() - 0.5) * 0.02;
velocities[i*3+2] = (Math.random() - 0.5) * 0.02;
}
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
const mat = new THREE.PointsMaterial({
size: 0.08, color: this.params.vortexColor,
blending: THREE.AdditiveBlending, transparent: true, opacity: 0.8
});
const particles = new THREE.Points(geo, mat);
this.scene.add(particles);
this.meshes.push(particles);
this.materials.push(mat);
}
createSpaceDistortion() {
const geo = new THREE.SphereGeometry(12, 64, 64);
const mat = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
dimensionShift: { value: 0 },
color1: { value: new THREE.Color(this.params.primaryColor) },
color2: { value: new THREE.Color(this.params.vortexColor) }
},
vertexShader: `
uniform float time;
uniform float dimensionShift;
varying vec3 vNorm;
varying vec3 vPos;
void main() {
vNorm = normal;
vPos = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform float dimensionShift;
uniform vec3 color1;
uniform vec3 color2;
varying vec3 vNorm;
varying vec3 vPos;
void main() {
vec3 viewDir = normalize(cameraPosition - vPos);
float fresnel = pow(1.0 - abs(dot(vNorm, viewDir)), 4.0);
float distortion = sin(vPos.x * 0.5 + time * 2.0) * cos(vPos.y * 0.7 + time * 1.5);
vec3 color = mix(color1, color2, distortion * 0.5 + 0.5 + dimensionShift * 0.3);
gl_FragColor = vec4(color, fresnel * 0.3);
}
`,
transparent: true, blending: THREE.AdditiveBlending, depthWrite: false
});
const distortion = new THREE.Mesh(geo, mat);
this.scene.add(distortion);
this.meshes.push(distortion);
this.materials.push(mat);
}
getRingColor(ring) {
const colors = [this.params.primaryColor, this.params.secondaryColor, this.params.accentColor, this.params.vortexColor];
return colors[ring % colors.length];
}
getCrystalColor(index) {
const colors = [this.params.accentColor, this.params.vortexColor, this.params.primaryColor, this.params.secondaryColor];
return colors[index % colors.length];
}
getStreamColor(index) {
const colors = [this.params.vortexColor, this.params.primaryColor, this.params.secondaryColor];
return colors[index % colors.length];
}
updatePortalUI() {
const indicator = document.getElementById('portal-indicator');
const status = document.getElementById('portal-status');
const fill = document.getElementById('portal-fill');
if (!indicator || !status || !fill) return;
fill.style.width = portalEnergy + '%';
if (portalEnergy > 80) {
status.textContent = 'Stable';
indicator.className = 'active';
} else if (portalEnergy > 50) {
status.textContent = 'Fluctuating';
indicator.className = 'active';
} else if (portalEnergy > 25) {
status.textContent = 'Unstable';
indicator.className = 'active';
} else {
status.textContent = 'Collapsed';
indicator.className = '';
}
}
setupEventListeners() {
document.getElementById('portal-btn').addEventListener('click', (e) => {
createRipple(e.currentTarget); this.activatePortal();
});
document.getElementById('reset-view').addEventListener('click', (e) => {
createRipple(e.currentTarget); this.controls.reset(); this.camera.position.copy(this.initialCameraPosition);
});
document.getElementById('randomize').addEventListener('click', (e) => {
createRipple(e.currentTarget); this.shiftDimensions();
});
window.addEventListener('resize', () => this.onResize());
window.addEventListener('dblclick', () => {
if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); }
else { document.exitFullscreen(); }
});
}
shiftDimensions() {
const colors = ['#9b59b6', '#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#e67e22', '#1abc9c', '#34495e'];
this.params.primaryColor = colors[Math.floor(Math.random() * colors.length)];
this.params.secondaryColor = colors[Math.floor(Math.random() * colors.length)];
this.params.accentColor = colors[Math.floor(Math.random() * colors.length)];
this.params.vortexColor = colors[Math.floor(Math.random() * colors.length)];
this.params.dimensionShift = Math.random();
this.createPortalScene();
}
setupGUI() {
const gui = new GUI();
gui.add(this.params, 'portalComplexity', 1, 8, 1).onChange(() => this.createPortalScene());
gui.add(this.params, 'crystalCount', 6, 20, 1).onChange(() => this.createPortalScene());
gui.addColor(this.params, 'accentColor').name('Portal Energy').onChange(c => {
this.portalMaterials.forEach(m => {
if(m.uniforms && m.uniforms.portalColor) m.uniforms.portalColor.value.set(c);
});
});
gui.add(this.params, 'bloomStrength', 0, 3, 0.1).onChange((v) => this.bloomPass.strength = v);
gui.add(this.params, 'rotationSpeed', 0, 1, 0.1);
gui.close();
}
animate() {
requestAnimationFrame(() => this.animate());
const delta = this.clock.getDelta();
this.time = this.clock.getElapsedTime();
if (portalEnergy < 100) {
portalEnergy = Math.min(100, portalEnergy + delta * 8);
this.updatePortalUI();
}
this.portalMaterials.forEach(shader => {
if(shader.uniforms) {
if(shader.uniforms.time) shader.uniforms.time.value = this.time;
if(shader.uniforms.dimensionShift) shader.uniforms.dimensionShift.value = this.params.dimensionShift;
}
});
this.materials.forEach(mat => {
if(mat.uniforms) {
if(mat.uniforms.time) mat.uniforms.time.value = this.time;
if(mat.uniforms.dimensionShift) mat.uniforms.dimensionShift.value = this.params.dimensionShift;
}
});
this.portalLights.forEach((light, i) => {
const angle = this.time * 0.3 + (i / 6) * Math.PI * 2;
const radius = 10 + Math.sin(this.time * 0.5 + i) * 3;
light.position.x = Math.cos(angle) * radius;
light.position.z = Math.sin(angle) * radius;
light.position.y = Math.sin(this.time * 0.4 + i * 0.7) * 5;
});
this.meshes.forEach((mesh, i) => {
if (!mesh.rotation) return;
const speed = this.params.rotationSpeed;
mesh.rotation.y += delta * speed * (i % 2 ? -1 : 1) * 0.3;
mesh.rotation.x += delta * speed * 0.1;
if (mesh.material && mesh.material.type === 'PointsMaterial') {
const positions = mesh.geometry.attributes.position.array;
for (let j = 0; j < positions.length; j += 3) {
positions[j] += Math.sin(this.time + j) * 0.001;
positions[j+1] += Math.cos(this.time + j) * 0.001;
positions[j+2] += Math.sin(this.time * 0.7 + j) * 0.001;
}
mesh.geometry.attributes.position.needsUpdate = true;
}
});
this.controls.update();
this.composer.render();
}
onResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.composer.setSize(window.innerWidth, window.innerHeight);
const pixelRatio = this.renderer.getPixelRatio();
this.fxaaPass.material.uniforms['resolution'].value.set(
1 / (window.innerWidth * pixelRatio), 1 / (window.innerHeight * pixelRatio)
);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new CosmicPortalVisualization());
} else {
new CosmicPortalVisualization();
}
</script>

WebGL Dimensional Portal with Three.js

An interactive 3D visualization using Three.js and WebGL. Button click activates energy pulses in this dynamic cosmic portal with custom shaders and post-processing effects.

A Pen by Techartist on CodePen.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment