codepens/particle-photos/dist/script.js

335 lines
9.7 KiB
JavaScript

import * as twgl from 'https://cdn.skypack.dev/twgl.js/dist/4.x/twgl-full.module';
import * as dat from 'https://cdn.skypack.dev/dat.gui';
import Stats from 'https://cdn.skypack.dev/stats.js';
import * as random from 'https://cdn.skypack.dev/@callumacrae/utils@0.2.4/built/random';
// This is mostly for displaying warnings
const isAndroid = /android/i.test(navigator.userAgent);
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isMobile = isAndroid || isIos;
const imageData = {
sunset: {
src:
'https://sketchbook.macr.ae/assets/particle-photos/leo-nagle-TLuNQu-5xP4-unsplash-small-blurred.jpg',
ratio: 2 / 3,
configPreset: {
particles: isMobile ? 20e3 : 40e3,
color: true,
radiusValExponent: 1.5,
alphaValMultiplier: 0.85,
alphaValExponent: 1.2,
pointSizeMultiplier: isMobile ? 20 : 10,
twinkleFactor: 0
}
},
woman: {
src:
'https://sketchbook.macr.ae/assets/particle-photos/thea-hoyer-CrJyu9HoeBg-unsplash-small-blurred.jpg',
ratio: 2 / 3,
configPreset: {
color: false,
particles: 30e3,
radiusValExponent: 2,
alphaValExponent: 1.5,
alphaValMultiplier: 0.85,
pointSizeMultiplier: 10,
twinkleFactor: 0
}
},
zebra: {
src:
'https://sketchbook.macr.ae/assets/particle-photos/frida-bredesen-c_cPNXlovvY-unsplash-small-blurred.png',
ratio: 1,
configPreset: {
color: false,
particles: 30e3,
radiusValExponent: 2,
alphaValExponent: 1.5,
alphaValMultiplier: 1,
pointSizeMultiplier: 10,
twinkleFrequency: 12,
twinkleIntensity: 25,
twinkleFactor: 0.7
}
},
'bucket hat': {
src: 'https://sketchbook.macr.ae/assets/particle-photos/brandon-webb-FwjkcL9Hpx8-unsplash-small.jpg',
ratio: 3 / 2,
configPreset: {
color: true,
radiusValExponent: 0.7,
alphaValExponent: 0.7,
alphaValMultiplier: 1,
pointSizeMultiplier: 7,
twinkleFactor: 0
}
},
rhino: {
src: 'https://sketchbook.macr.ae/assets/particle-photos/rhino.jpeg',
ratio: 422 / 222,
configPreset: {
particles: 50e3,
radiusValExponent: 1.3,
alphaValExponent: 0.1,
alphaValMultiplier: 1,
color: true,
pointSizeMultiplier: 8,
twinkleFactor: 0.8
}
}
};
new Vue({
el: '.canvas-container',
data: () => {
const image = 'rhino';
return {
status: 'playing',
width: undefined,
height: undefined,
showHelp: false,
showIosWarning: false,
userTexture: undefined,
config: {
// Init config
particles: 40e3,
particleBaseSpeed: isMobile ? 8 : 5,
// Shader config
radiusValExponent: 1.5,
alphaValExponent: 1.2,
alphaValMultiplier: 0.85,
color: true,
xInNoiseMultiplier: 200, // Not user configurable, bit buggy
xOutNoiseMultiplier: isMobile ? 0.4 : 0.2,
yInNoiseMultiplier: 1234, // Not user configurable, pointless
yOutNoiseMultiplier: isMobile ? 0.004 : 0.002,
pointSizeMultiplier: 10,
twinkleFrequency: 12,
twinkleIntensity: 25,
twinkleFactor: 0,
image,
...imageData[image].configPreset
}
};
},
mounted() {
document.querySelector('.loading').style.display = 'none';
this.$el.style.display = 'flex';
this.setSize();
this.init().then(() => {
this.frame();
});
const gui = new dat.GUI();
this.gui = gui;
gui
.add(this.config, 'image', Object.keys(imageData).concat('user'))
.listen();
gui.add(this.config, 'particles', 5000, 100000, 1000).listen();
gui.add(this.config, 'particleBaseSpeed', 0, 50).listen();
gui.add(this.config, 'radiusValExponent', 0.1, 10).listen();
gui.add(this.config, 'alphaValExponent', 0.1, 10).listen();
gui.add(this.config, 'alphaValMultiplier', 0.1, 1).listen();
gui.add(this.config, 'color').listen();
gui.add(this.config, 'xOutNoiseMultiplier', 0, 1).listen();
gui.add(this.config, 'yOutNoiseMultiplier', 0, 0.1).listen();
gui.add(this.config, 'pointSizeMultiplier', 1, 30).listen();
gui.add(this.config, 'twinkleFrequency', 1, 50).listen();
gui.add(this.config, 'twinkleIntensity', 1, 50).listen();
gui.add(this.config, 'twinkleFactor', 0, 1).listen();
this.stats = new Stats();
this.stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(this.stats.dom);
if (isIos) {
this.status = 'paused';
this.showIosWarning = true;
gui.close();
}
},
beforeDestroy() {
cancelAnimationFrame(this.frameId);
if (this.gui) {
this.gui.destroy();
}
if (this.stats) {
this.stats.dom.remove();
delete this.stats;
}
},
methods: {
setSize() {
const canvas = this.$refs.canvas;
this.gl = canvas.getContext('webgl', { preserveDrawingBuffer: true });
this.dpr = window.devicePixelRatio;
if (window.innerWidth < window.innerHeight * this.imageData.ratio) {
this.width = window.innerWidth * this.dpr;
this.height = (window.innerWidth / this.imageData.ratio) * this.dpr;
} else {
this.height = window.innerHeight * this.dpr;
this.width = window.innerHeight * this.imageData.ratio * this.dpr;
}
canvas.width = this.width;
canvas.height = this.height;
},
init() {
const { gl, config } = this;
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
this.programInfo = twgl.createProgramInfo(gl, ['vs', 'fs']);
const particleData = {
xs: [],
initialOffsets: [],
speeds: []
};
for (let i = 0; i < config.particles; i++) {
particleData.xs.push(i / config.particles);
particleData.initialOffsets.push(random.range(-1, 1));
particleData.speeds.push(random.range(0, config.particleBaseSpeed));
}
this.particleData = particleData;
twgl.setAttributePrefix('a_');
this.bufferInfo = twgl.createBufferInfoFromArrays(gl, {
x: {
numComponents: 1,
data: particleData.xs
},
initial_offset: {
numComponents: 1,
data: particleData.initialOffsets
},
speed: {
numComponents: 1,
data: particleData.speeds
}
});
twgl.setBuffersAndAttributes(gl, this.programInfo, this.bufferInfo);
if (this.images) {
return Promise.resolve();
}
return new Promise(resolve => {
// Use a blurred image instead of sampling an area of more than one pixel
// in the fragment shader - do the work ahead of time!
this.images = twgl.createTextures(gl, imageData, resolve);
});
},
frame(timestamp = 0) {
if (this.status !== 'recording') {
this.frameId = requestAnimationFrame(this.frame);
}
if (this.status === 'paused') {
return;
}
this.stats.begin();
const { gl, programInfo, bufferInfo, width, height, config } = this;
gl.viewport(0, 0, width, height);
gl.useProgram(programInfo.program);
gl.clear(gl.COLOR_BUFFER_BIT);
const uniforms = {
u_time: timestamp,
u_image_texture:
config.image === 'user'
? this.userTexture.texture
: this.images[config.image],
u_width: width,
u_height: height,
u_dpr: this.dpr,
u_radius_val_exponent: config.radiusValExponent,
u_alpha_val_exponent: config.alphaValExponent,
u_alpha_val_multiplier: config.alphaValMultiplier,
u_color: config.color,
u_x_in_noise_multiplier: config.xInNoiseMultiplier,
u_x_out_noise_multiplier: config.xOutNoiseMultiplier,
u_y_in_noise_multiplier: config.yInNoiseMultiplier,
u_y_out_noise_multiplier: config.yOutNoiseMultiplier,
u_point_size_multiplier: config.pointSizeMultiplier,
u_twinkle_frequency: config.twinkleFrequency,
u_twinkle_intensity: config.twinkleIntensity,
u_twinkle_factor: config.twinkleFactor
};
twgl.setUniforms(programInfo, uniforms);
twgl.drawBufferInfo(gl, bufferInfo, gl.POINTS);
this.stats.end();
},
handleDrop(e) {
const image = e.dataTransfer.files[0];
if (!image.type.startsWith('image/')) {
return;
}
const reader = new FileReader();
reader.onload = () => {
const img = document.createElement('img');
img.onload = () => {
const texture = twgl.createTexture(this.gl, { src: img });
this.userTexture = { texture, ratio: img.width / img.height };
this.config.image = 'user';
this.setSize();
if (img.width > 500 && img.height > 500) {
setTimeout(() => {
alert(
'Heads up, this works better with low resolution images: see the help for more info'
);
});
}
};
img.src = reader.result;
};
reader.readAsDataURL(image);
}
},
computed: {
imageData() {
return this.config.image === 'user'
? this.userTexture
: imageData[this.config.image];
}
},
watch: {
'config.particles': 'init',
'config.particleBaseSpeed': 'init',
'config.image'(image, oldImage) {
if (image === 'user' && !this.userTexture) {
alert('Drag and drop an image onto this window to test your own image');
this.config.image = oldImage;
return;
}
this.setSize();
if (this.imageData.configPreset) {
for (let [key, value] of Object.entries(this.imageData.configPreset)) {
this.config[key] = value;
}
}
}
}
});