moh1456's picture
Upload 27 files
af8291a verified
import MiniGL from './MiniGL.js';
import Blend from './ShadersJs/Blend.js';
import Fragment from './ShadersJs/Fragment.js';
import Noise from './ShadersJs/Noise.js';
import Vertex from './ShadersJs/Vertex.js';
import Uniform from './Uniform.js';
import Material from './Material.js';
import Mesh from './Mesh.js';
import PlaneGeometry from './PlaneGeometry.js';
class Gradient {
/**
* Class reference used primarily for static properties.
*
* @type {Gradient}
*/
_class = Gradient;
/**
* Default options used if user doesn't provide a value.
*
* @type {object}
*/
static defaultOptions = {
canvas: null,
colors: ['#f00', '#0f0', '#00f'],
wireframe: false,
density: [.06, .16],
angle: 0,
amplitude: 320,
static: false, // Enable non-animating gradient
loadedClass: 'is-loaded',
zoom: 1, // @todo not used.
speed: 5, // @todo not used.
rotation: 0, // @todo not used.
};
vertexShader = null;
uniforms = null;
time = 1253106; // @todo work out why this number has been choosen.
mesh = null;
material = null;
geometry = null;
// @todo tidy up these properties
scrollingTimeout = null;
scrollingRefreshDelay = 200;
scrollObserver = null;
width = null;
minWidth = 1111;
height = 600;
xSegCount = null;
ySegCount = null;
freqX = 0.00014;
freqY = 0.00029;
freqDelta = 0.00001;
activeColors = [1, 1, 1, 1];
/**
* @type {{fragment: string, vertex: string, blend: string, noise: string}}
*/
shaderFiles = {
vertex: Vertex,
noise: Noise,
blend: Blend,
fragment: Fragment
};
/**
* User defined options
*
* @type {object}
*/
options = {};
/**
* Store arbitrary flags consisting of mainly boolean values but in some cases can be any data type.
* @type {object}
* @private
*/
_flags = {
playing: true // autoplay on init
};
/**
* Cached canvas element.
*
* @type {HTMLCanvasElement|null}
* @private
*/
_canvas;
/**
* @type {WebGLRenderingContext|null}
* @private
*/
_context;
/**
* Cached MiniGL instance.
*
* @type {MiniGL}
* @private
*/
_minigl;
/**
* @param {object} options
*/
constructor(options) {
this.options = options;
// Find and store the canvas element.
this.setCanvas(this.findCanvas(this.getOption('canvas')));
// Bail if no canvas element.
if (!this.getCanvas()) {
throw 'Missing Canvas. Pass the canvas to the Gradient constructor.';
}
// Initiate the MiniGL controller.
this._minigl = new MiniGL(this.getCanvas(), this.getCanvas().offsetWidth, this.getCanvas().offsetHeight);
// Initiate the canvas gradient.
this.init();
}
/**
* Get a user or default option.
*
* @param {string} name
* @param defaultValue
* @returns {*}
*/
getOption(name, defaultValue = undefined) {
if (defaultValue === undefined && name in this._class.defaultOptions) {
defaultValue = this._class.defaultOptions[name];
}
return name in this.options ? this.options[name] : defaultValue;
}
/**
* Get the canvas element and cache as private property.
*
* @param {string|HTMLCanvasElement} selector
* @returns {HTMLCanvasElement|null}
*/
findCanvas(selector) {
const canvas = typeof selector === 'string' ? document.querySelector(selector) : selector;
if (canvas instanceof HTMLCanvasElement) {
return canvas;
}
return null;
}
/**
* Sets the `_canvas` and `_context` properties.
*
* @param {HTMLCanvasElement} canvas
*/
setCanvas(canvas) {
if (canvas) {
this._canvas = canvas;
this._context = canvas.getContext('webgl', {
antialias: true
});
} else {
this._canvas = null;
this._context = null;
}
}
/**
* @return {HTMLCanvasElement}
*/
getCanvas() {
return this._canvas;
}
/**
* @return {WebGLRenderingContext}
*/
getContext() {
return this._context;
}
/**
* @param {string} name
* @param {*} value
* @return {*}
*/
setFlag(name, value) {
return this._flags[name] = value;
}
/**
* @param {string} name
* @param defaultValue
* @return {boolean|undefined}
*/
getFlag(name, defaultValue = undefined) {
return this._flags[name] || defaultValue;
}
handleScroll() {
clearTimeout(this.scrollingTimeout);
this.scrollingTimeout = setTimeout(this.handleScrollEnd, this.scrollingRefreshDelay);
if (this.getFlag('playing')) {
this.setFlag('isScrolling', true);
this.pause();
}
}
handleScrollEnd() {
this.setFlag('isScrolling', false);
if (this.getFlag('isIntersecting')) {
this.play();
}
}
/**
* @todo Update resize method to use canvas size not window.
*/
resize() {
const [ densityX, densityY ] = this.getOption('density');
this.width = window.innerWidth;
this._minigl.setSize(this.width, this.height);
this._minigl.setOrthographicCamera();
this.xSegCount = Math.ceil(this.width * densityX);
this.ySegCount = Math.ceil(this.height * densityY);
this.mesh.geometry.setTopology(this.xSegCount, this.ySegCount);
this.mesh.geometry.setSize(this.width, this.height);
this.mesh.material.uniforms.u_shadow_power.value = this.width < 600 ? 5 : 6;
}
animate(event = 0) {
const shouldSkipFrame = !!window.document.hidden || (!this.getFlag('playing') || parseInt(event, 10) % 2 === 0);
let lastFrame = this.getFlag('lastFrame', 0);
if (!shouldSkipFrame) {
this.time += Math.min(event - lastFrame, 1000 / 15);
lastFrame = this.setFlag('lastFrame', event);
this.mesh.material.uniforms.u_time.value = this.time;
this._minigl.render();
}
// @todo support static gradient.
if (lastFrame !== 0 && this.getOption('static')) {
this._minigl.render();
return this.disconnect();
}
if (/*this.getFlag('isIntersecting') && */this.getFlag('playing')) {
requestAnimationFrame(this.animate.bind(this));
}
}
/**
* Pause the animation.
*/
pause() {
this.setFlag('playing', false);
}
/**
* Start or continue the animation.
*/
play() {
requestAnimationFrame(this.animate.bind(this));
this.setFlag('playing', true);
}
disconnect() {
if (this.scrollObserver) {
window.removeEventListener("scroll", this.handleScroll);
this.scrollObserver.disconnect();
}
window.removeEventListener("resize", this.resize);
}
initMaterial() {
/**
* @type {array[]}
*/
const colors = this.getOption('colors').map(hex => {
// Check if shorthand hex value was used and double the length so the conversion in normalizeColor will work.
if (hex.length === 4) {
const hexTemp = hex.substr(1).split('').map(hexTemp => hexTemp + hexTemp).join('');
hex = `#${hexTemp}`
}
return hex && `0x${hex.substr(1)}`;
}).filter(Boolean).map(this.normalizeColor);
this.uniforms = {
u_time: new Uniform(this._minigl, 'float',0),
u_shadow_power: new Uniform(this._minigl, 'float', 10),
u_darken_top: new Uniform(this._minigl, 'float', this.getCanvas().dataset.jsDarkenTop ? 1 : 0),
u_active_colors: new Uniform(this._minigl, 'vec4', this.activeColors),
u_global: new Uniform(this._minigl, 'struct', {
noiseFreq: new Uniform(this._minigl, 'vec2', [this.freqX, this.freqY]),
noiseSpeed: new Uniform(this._minigl, 'float',0.000005)
}),
u_vertDeform: new Uniform(this._minigl, 'struct', {
incline: new Uniform(this._minigl, 'float', Math.sin(this.getOption('angle')) / Math.cos(this.getOption('angle'))),
offsetTop: new Uniform(this._minigl, 'float', -0.5),
offsetBottom: new Uniform(this._minigl, 'float', -0.5),
noiseFreq: new Uniform(this._minigl, 'vec2', [3, 4]),
noiseAmp: new Uniform(this._minigl, 'float', this.getOption('amplitude')),
noiseSpeed: new Uniform(this._minigl, 'float', 10),
noiseFlow: new Uniform(this._minigl, 'float', 3),
noiseSeed: new Uniform(this._minigl, 'float', this.seed)
}, {
excludeFrom: 'fragment'
}),
u_baseColor: new Uniform(this._minigl, 'vec3', colors[0], {
excludeFrom: 'fragment'
}),
u_waveLayers: new Uniform(this._minigl, 'array', [], {
excludeFrom: 'fragment'
})
};
for (let e = 1; e < colors.length; e += 1) {
const waveLayerUniform = new Uniform(this._minigl, 'struct', {
color: new Uniform(this._minigl, 'vec3', colors[e]),
noiseFreq: new Uniform(this._minigl, 'vec2', [2 + e / colors.length, 3 + e / colors.length]),
noiseSpeed: new Uniform(this._minigl, 'float', 11 + 0.3 * e),
noiseFlow: new Uniform(this._minigl, 'float', 6.5 + 0.3 * e),
noiseSeed: new Uniform(this._minigl, 'float', this.seed + 10 * e),
noiseFloor: new Uniform(this._minigl, 'float', 0.1),
noiseCeil: new Uniform(this._minigl, 'float', 0.63 + 0.07 * e)
});
this.uniforms.u_waveLayers.value.push(waveLayerUniform);
}
this.vertexShader = [
this.shaderFiles.noise,
this.shaderFiles.blend,
this.shaderFiles.vertex
].join("\n\n");
return new Material(this._minigl, this.vertexShader, this.shaderFiles.fragment, this.uniforms);
}
initMesh() {
this.material = this.initMaterial();
this.geometry = new PlaneGeometry(this._minigl);
this.mesh = new Mesh(this._minigl, this.geometry, this.material);
this.mesh.wireframe = this.getOption('wireframe');
}
updateFrequency(e) {
this.freqX += e;
this.freqY += e;
}
toggleColor(index) {
this.activeColors[index] = this.activeColors[index] === 0 ? 1 : 0;
}
init() {
// Add loaded class.
const loadedClass = this.getOption('loadedClass');
if (loadedClass) {
this.getCanvas().classList.add(loadedClass);
}
// @todo add scroll observer.
//
// this.scrollObserver = await s.create(.1, false),
// this.scrollObserver.observe(this.getCanvas());
//
// this.scrollObserver.onSeparate(() => {
// window.removeEventListener("scroll", this.handleScroll);
//
// this.setFlag('isIntersecting', false);
//
// if (this.getFlag('playing')) {
// this.pause();
// }
// });
//
// this.scrollObserver.onIntersect(() => {
// window.addEventListener("scroll", this.handleScroll);
//
// this.setFlag('isIntersecting', true);
//
// this.addIsLoadedClass();
// this.play();
// });
this.initMesh();
this.resize();
requestAnimationFrame(this.animate.bind(this));
window.addEventListener('resize', this.resize);
}
/**
* @param {string} hexCode
* @return {number[]}
*/
normalizeColor(hexCode) {
return [
(hexCode >> 16 & 255) / 255,
(hexCode >> 8 & 255) / 255,
(255 & hexCode) / 255
];
}
}
window.Gradient = Gradient;
if (window.jQuery) {
jQuery.fn.gradient = function(options = {}) {
options.canvas = this.get(0);
this._gradient = new Gradient(options);
return this;
};
}