Mike.Whitehead
Mike.Whitehead

Reputation: 818

Javascript - interactive particle logo not working

I'm working through instructions to construct an interactive particle logo design and can't seem to get to the finished product. This is the logo image file -

Havoc Creative logo

I'm using a canvas structure / background. Here's the code -

var canvasInteractive = document.getElementById('canvas-interactive');
var canvasReference = document.getElementById('canvas-reference');

var contextInteractive = canvasInteractive.getContext('2d');
var contextReference = canvasReference.getContext('2d');

var image = document.getElementById('img');

var width = canvasInteractive.width = canvasReference.width = window.innerWidth;
var height = canvasInteractive.height = canvasReference.height = window.innerHeight;

var logoDimensions = {
  x: 500,
  y: 500
};

var center = {
  x: width / 2,
  y: height / 2
};

var logoLocation = {
  x: center.x - logoDimensions.x / 2,
  y: center.y - logoDimensions.y / 2
};

var mouse = {
  radius: Math.pow(100, 2),
  x: 0,
  y: 0
};

var particleArr = [];
var particleAttributes = {
  friction: 0.95,
  ease: 0.19,
  spacing: 6,
  size: 4,
  color: "#ffffff"
};

function Particle(x, y) {
  this.x = this.originX = x;
  this.y = this.originY = y;
  this.rx = 0;
  this.ry = 0;
  this.vx = 0;
  this.vy = 0;
  this.force = 0;
  this.angle = 0;
  this.distance = 0;
}

Particle.prototype.update = function() {
  this.rx = mouse.x - this.x;
  this.ry = mouse.y - this.y;
  this.distance = this.rx * this.rx + this.ry * this.ry;
  this.force = -mouse.radius / this.distance;
  if (this.distance < mouse.radius) {
    this.angle = Math.atan2(this.ry, this.rx);
    this.vx += this.force * Math.cos(this.angle);
    this.vy += this.force * Math.sin(this.angle);
  }
  this.x += (this.vx *= particleAttributes.friction) + (this.originX - this.x) * particleAttributes.ease;
  this.y += (this.vy *= particleAttributes.friction) + (this.originY - this.y) * particleAttributes.ease;
};

function init() {
  contextReference.drawImage(image, logoLocation.x, logoLocation.y);
  var pixels = contextReference.getImageData(0, 0, width, height).data;
  var index;
  for (var y = 0; y < height; y += particleAttributes.spacing) {
    for (var x = 0; x < width; x += particleAttributes.spacing) {
      index = (y * width + x) * 4;
      if (pixels[++index] > 0) {
        particleArr.push(new Particle(x, y));
      }
    }
  }
};
init();

function update() {
  for (var i = 0; i < particleArr.length; i++) {
    var p = particleArr[i];
    p.update();
  }
};

function render() {
  contextInteractive.clearRect(0, 0, width, height);
  for (var i = 0; i < particleArr.length; i++) {
    var p = particleArr[i];
    contextInteractive.fillStyle = particleAttributes.color;
    contextInteractive.fillRect(p.x, p.y, particleAttributes.size, particleAttributes.size);
  }
};

function animate() {
  update();
  render();
  requestAnimationFrame(animate);
}
animate();

document.body.addEventListener("mousemove", function(event) {
  mouse.x = event.clientX;
  mouse.y = event.clientY;
});

document.body.addEventListener("touchstart", function(event) {
  mouse.x = event.changedTouches[0].clientX;
  mouse.y = event.changedTouches[0].clientY;
}, false);

document.body.addEventListener("touchmove", function(event) {
  event.preventDefault();
  mouse.x = event.targetTouches[0].clientX;
  mouse.y = event.targetTouches[0].clientY;
}, false);

document.body.addEventListener("touchend", function(event) {
  event.preventDefault();
  mouse.x = 0;
  mouse.y = 0;
}, false);
html,
body {
  margin: 0px;
  position: relative;
  background-color: #000;
}

canvas {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
}

img {
  display: none;
  width: 70%;
  height: 400px;
  position: absolute;
  left: 50%;
  transform: translate(-50%, 30%);
}
<html>

<body>
  <canvas id="canvas-interactive"></canvas>
  <canvas id="canvas-reference"></canvas>

  <img src="https://i.sstatic.net/duv9h.png" alt="..." id="img">

</body>

</html>

My understanding is the image file has to be set to display: none; and then the image needs to be re-drawn using the javascript commands but I'm not sure if this image is compatible or not. When finished I want the image on a white background. By way of an example the end design needs to resemble this - Logo particle design

Upvotes: 1

Views: 941

Answers (2)

Anas
Anas

Reputation: 1513

Use png saved as PNG-8 and and allow cross-origin

I saw the cool article from Bricks and mortar and thought I'd try it out.

I battled with it for an eternity, thinking that my js was wrong... Turns out that the image has to be saved as a PNG-8 without dither instead of a PNG-24.

Then make sure that you add the crossOrigin="Anonymous" attribute to the image tag:

<img crossOrigin="Anonymous" id="img" src="[link to wherever you host the image]" alt="logo">

I also hid the reference canvas by adding the following styles:

canvas#canvas-reference {
  display: none;
}

Photoshop settings for when you save the png

I also added a debounce and resize function, so it's responsive.

The result:

enter image description here

See Demo with inverted logo

Upvotes: 0

Blindman67
Blindman67

Reputation: 54026

Particle positions from bitmap.

To get the FX you want you need to create a particle system. This is just an array of objects, each with a position, the position where they want to be (Home), a vector defining their current movement, and the colour.

You get each particle's home position and colour by reading pixels from the image. You can access pixel data by rendering an image on a canvas and the using ctx.getImageData to get the pixel data (Note image must be on same domain or have CORS headers to access pixel data). As you read each pixel in turn, if not transparent, create a particle for that pixel and set it colour and home position from the pixels colour and position.

Use requestAnimationFrame to call a render function that every frame iterates all the particles moving them by some set of rules that give you the motion you are after. Once you have move each particle, render them to the canvas using simple shapes eg fillRect

Mouse interaction

To have interaction with the mouse you will need to use mouse move events to keep track of the mouse position relative to the canvas you are rendering to. As you update each particle you also check how far it is from the mouse. You can then push or pull the particle from or to the mouse (depending on the effect you want.

Rendering speed will limit the particle count.

The only issue with these types of FX is that you will be pushing the rendering speed limits as the particle count goes up. What may work well on one machine, will run very slow on another.

To avoid being too slow, and not looking good on some machines you should consider keeping an eye on the frame rate and reducing the particle count if it runs slow. To compensate you can increase the particle size or even reduce the canvas resolution.

The bottleneck is the actual rendering of each particle. When you get to large numbers the path methods really grinds down. If you want really high numbers you will have to render pixels directly to the bitmap, using the same method as reading but in reverse of course.

Example simple particles read from bitmap.

The example below uses text rendered to a canvas to create the particles, and to use an image you would just draw the image rather than the text. The example is a bit overkill as I ripped it from an old answer of mine. It is just as an example of the various ways to get stuff done.

const ctx = canvas.getContext("2d");

const Vec = (x, y) => ({x, y});
const setStyle = (ctx,style) => {    Object.keys(style).forEach(key => ctx[key] = style[key]) }
const createImage = (w,h) => {var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i}
const textList = ["Particles"];
var textPos = 0;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center 
var ch = h / 2;
var globalTime;
var started = false;
requestAnimationFrame(update);

const mouse  = {x : 0, y : 0, button : false}
function mouseEvents(e){
	mouse.x = e.pageX;
	mouse.y = e.pageY;
	mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));

function onResize(){ 
	cw = (w = canvas.width = innerWidth) / 2;
	ch = (h = canvas.height = innerHeight) / 2;
    if (!started) { startIt() }
}

function update(timer){
    globalTime = timer;
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
	if (w !== innerWidth || h !== innerHeight){ onResize() }
	else { ctx.clearRect(0,0,w,h) }
    particles.update();
    particles.draw();	
    requestAnimationFrame(update);
}


function createParticles(text){
    createTextMap(
        text, 60, "Arial", 
        {   fillStyle : "#FF0", strokeStyle : "#F00", lineWidth : 2, lineJoin : "round", },
        { top : 0, left : 0, width : canvas.width, height : canvas.height }
    )
}
// This function starts the animations
function startIt(){
    started = true;
    const next = ()=>{
        var text = textList[(textPos++ ) % textList.length];
        createParticles(text);
        setTimeout(moveOut,text.length * 100 + 12000);
    }
    const moveOut = ()=>{
        particles.moveOut();
        setTimeout(next,2000);
    }
    setTimeout(next,0);
}



// the following function create the particles from text using a canvas
// the canvas used is displayed on the main canvas top left fro reference.
var tCan = createImage(100, 100); // canvas used to draw text
function createTextMap(text,size,font,style,fit){
    const hex = (v)=> (v < 16 ? "0" : "") + v.toString(16);
    tCan.ctx.font = size + "px " + font;
    var width = Math.ceil(tCan.ctx.measureText(text).width + size);
    tCan.width = width;
    tCan.height = Math.ceil(size *1.2);
    var c = tCan.ctx;
    c.font = size + "px " + font;
    c.textAlign = "center";
    c.textBaseline = "middle";
    setStyle(c,style);
    if (style.strokeStyle) { c.strokeText(text, width / 2, tCan.height / 2) }
    if (style.fillStyle) { c.fillText(text, width / 2, tCan.height/ 2) }
    particles.empty();
    var data = c.getImageData(0,0,width,tCan.height).data;
    var x,y,ind,rgb,a;
    for(y = 0; y < tCan.height; y += 1){
        for(x = 0; x < width; x += 1){
            ind = (y * width + x) << 2;  // << 2 is equiv to * 4
            if(data[ind + 3] > 128){  // is alpha above half
                rgb = `#${hex(data[ind ++])}${hex(data[ind ++])}${hex(data[ind ++])}`;
                particles.add(Vec(x, y), Vec(x, y), rgb);
            }
        }
    }
    particles.sortByCol
    var scale = Math.min(fit.width / width, fit.height / tCan.height);
    particles.each(p=>{
        p.home.x = ((fit.left + fit.width) / 2) + (p.home.x - (width / 2)) * scale;
        p.home.y = ((fit.top + fit.height) / 2) + (p.home.y - (tCan.height / 2)) * scale;

    })
        .findCenter() // get center used to move particles on and off of screen
        .moveOffscreen()  // moves particles off the screen
        .moveIn();        // set the particles to move into view.

}

// basic particle
const particle = { pos : null,  delta : null, home : null, col : "black", }
// array of particles
const particles = {
    items : [], // actual array of particles
    mouseFX : {  power : 12,dist :110, curve : 2, on : true },
    fx : { speed : 0.3, drag : 0.6, size : 4, jiggle : 1 },
    // direction 1 move in -1 move out
    direction : 1,
    moveOut () {this.direction = -1; return this},
    moveIn () {this.direction = 1; return this},
    length : 0, 
    each(callback){ // custom iteration 
        for(var i = 0; i < this.length; i++){   callback(this.items[i],i) }
        return this;
    },
    empty() { this.length = 0; return this },
    deRef(){  this.items.length = 0; this.length = 0 },
    sortByCol() {  this.items.sort((a,b) => a.col === b.col ? 0 : a.col < b.col ? 1 : -1 ) },
    add(pos, home, col){  // adds a particle
        var p;
        if(this.length < this.items.length){
            p = this.items[this.length++];
            p.home.x = home.x;
			p.home.y = home.y;
            p.delta.x = 0;
            p.delta.y = 0;
            p.col = col;
        }else{
            this.items.push( Object.assign({}, particle,{ pos, home, col, delta : Vec(0,0) } ) );
            this.length = this.items.length
        }
        return this;
    },
    draw(){ // draws all
        var p, size, sizeh;
        sizeh = (size = this.fx.size) / 2;
        for(var i = 0; i < this.length; i++){
            p = this.items[i];
            ctx.fillStyle = p.col;
            ctx.fillRect(p.pos.x - sizeh, p.pos.y - sizeh, size, size);
        }
    },
    update(){ // update all particles
        var p,x,y,d;
        const mP = this.mouseFX.power;
        const mD = this.mouseFX.dist;
        const mC = this.mouseFX.curve;
        const fxJ = this.fx.jiggle;
        const fxD = this.fx.drag;
        const fxS = this.fx.speed;

        for(var i = 0; i < this.length; i++){
            p = this.items[i];
            p.delta.x += (p.home.x - p.pos.x ) * fxS + (Math.random() - 0.5) * fxJ;
            p.delta.y += (p.home.y - p.pos.y ) * fxS + (Math.random() - 0.5) * fxJ;
            p.delta.x *= fxD;
            p.delta.y *= fxD;
            p.pos.x += p.delta.x * this.direction;
            p.pos.y += p.delta.y * this.direction;
            if(this.mouseFX.on){
                x = p.pos.x - mouse.x;
                y = p.pos.y - mouse.y;
                d = Math.sqrt(x * x + y * y);
                if(d < mD){
                    x /= d;
                    y /= d;
                    d /= mD;
                    d = (1-Math.pow(d, mC)) * mP;
                    p.pos.x += x * d;
                    p.pos.y += y * d;        
                }
            }
        }
        return this;
    },
    findCenter(){  // find the center of particles maybe could do without
        var x,y;
        y = x = 0;
        this.each(p => { x += p.home.x; y += p.home.y });
        this.center = Vec(x / this.length, y / this.length);
        return this;
    },
    moveOffscreen(){  // move start pos offscreen
        var dist,x,y;
        dist = Math.sqrt(this.center.x * this.center.x + this.center.y * this.center.y);
        
        this.each(p => {
            var d;
            x = p.home.x - this.center.x;
            y = p.home.y - this.center.y;
            d =  Math.max(0.0001,Math.sqrt(x * x + y * y)); // max to make sure no zeros
            p.pos.x = p.home.x + (x / d)  * dist;
            p.pos.y = p.home.y + (y / d)  * dist;
        });
        return this;
    },
}
canvas { position : absolute; top : 0px; left : 0px; background : black;}
<canvas id="canvas"></canvas>

Upvotes: 1

Related Questions