Skip to content

Instantly share code, notes, and snippets.

@shshaw
Last active December 1, 2016 16:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shshaw/0dd1a6ab24b4fa253c076377ec885eaf to your computer and use it in GitHub Desktop.
Save shshaw/0dd1a6ab24b4fa253c076377ec885eaf to your computer and use it in GitHub Desktop.
Smudge & Randomize Image with PIXI.js Mesh
console.clear();
let mesh;
let cloth;
let spacingX = 5;
let spacingY = 5;
let opts = {
image: 'http://brokensquare.com/Code/assets/face.png',
pointsX: 40,
pointsY: 40,
brushSize: 30,
randomImage(){
this.image = 'https://unsplash.it/400/400?image=' + Math.floor(Math.random() * 1100);
if ( cloth ) { cloth.randomize(loadTexture); }
else { loadTexture(); }
},
reset(){
if ( cloth ) { cloth.reset(); }
},
randomizePoints(){
if ( cloth ) { cloth.randomize(); }
}
};
/*////////////////////////////////////////*/
let gui = new dat.GUI();
gui.closed = window.innerWidth < 600;
let image = gui.add(opts, 'image', {
Face: 'http://brokensquare.com/Code/assets/face.png',
Lion: 'https://unsplash.it/400/400?image=1074',
Water: 'https://unsplash.it/400/400?image=1053',
YellowCurtain: 'https://unsplash.it/400/400?image=855',
Tunnel: 'https://unsplash.it/400/400?image=137'
});
image.onChange(loadTexture);
let random = gui.add(opts, 'randomImage');
let randomize = gui.add(opts, 'randomizePoints');
let reset = gui.add(opts, 'reset');
/*////////////////////////////////////////*/
let mouse = {
down: false,
x: 0,
y: 0,
px: 0,
py: 1
}
let brush = new PIXI.Graphics();
function updateBrush(){
brush.clear();
brush.blendMode = PIXI.BLEND_MODES.ADD;
brush.lineStyle(1, 0x888888, 0.4);
brush.drawCircle(0, 0, opts.brushSize); // drawCircle(x, y, radius)
brush.x = mouse.x;
brush.y = mouse.y;
brush.updateLocalBounds();
}
updateBrush();
let influence = gui.add(opts, 'brushSize', 0, 100).step(1);
influence.onChange(updateBrush);
/*////////////////////////////////////////*/
let stage = new PIXI.Container();
stage.addChild(brush);
let renderer = PIXI.autoDetectRenderer(window.innerWidth, window.innerHeight, { transparent: true });
document.body.appendChild(renderer.view);
renderer.render(stage);
/*////////////////////////////////////////*/
function loadTexture() {
console.log('loading texture', opts.image);
document.body.className = 'loading';
let texture = new PIXI.Texture.fromImage(opts.image);
if ( !texture.requiresUpdate ) { texture.update(); }
texture.on('error', function(){ console.error('AGH!'); });
texture.on('update',function(){
document.body.className = '';
console.log('texture loaded');
if ( mesh ) { stage.removeChild(mesh); }
mesh = new PIXI.mesh.Plane( this, opts.pointsX, opts.pointsY);
mesh.width = this.width;
mesh.height = this.height;
mesh.x = renderer.width / 2 - mesh.width / 2;
mesh.y = renderer.height / 2 - mesh.height / 2;
spacingX = mesh.width / (opts.pointsX-1);
spacingY = mesh.height / (opts.pointsY-1);
cloth = new Cloth(opts.pointsX-1, opts.pointsY-1, !opts.pinCorners);
stage.addChildAt(mesh,0);
});
}
loadTexture(opts.image);
/*////////////////////////////////////////*/
;(function update() {
requestAnimationFrame(update);
if ( cloth ) { cloth.update(0.016) }
brush.x = mouse.x;
brush.y = mouse.y;
renderer.render(stage);
})(0)
/*////////////////////////////////////////*/
const twoPi = Math.PI * 2;
const ease = Elastic.easeOut.config(1.2, 0.4);
class Point {
constructor (x, y) {
this.x = this.origX = x
this.y = this.origY = y
this.randomize(this.reset.bind(this));
}
animateTo(nx, ny, force, callback){
if ( !this.resetting || force ) {
let dx = nx - this.x
let dy = ny - this.y
let dist = Math.sqrt(dx * dx + dy * dy)
this.resetting = true;
TweenMax.to(this,
Math.min(1.25, Math.max(0.4, dist / 40) ),
{
x: nx,
y: ny,
ease: ease,
onComplete: () => {
this.resetting = false
if ( callback ) { callback(); }
}
})
} else if ( callback ) { callback(); }
}
randomize(callback) {
let nx = this.x + ((Math.random() * 60) - 30);
let ny = this.y + ((Math.random() * 60) - 30);
this.animateTo(nx, ny, null, callback ? callback : null );
}
reset(){
this.animateTo(this.origX, this.origY, true);
}
update (delta) {
let dx;
let dy;
if (!this.resetting && mouse.down) {
dx = this.x - mouse.x + mesh.x
dy = this.y - mouse.y + mesh.y
let dist = Math.sqrt(dx * dx + dy * dy)
if ( dist < opts.brushSize) {
this.x = this.x + (( mouse.x - mouse.px) * Math.abs( Math.cos( twoPi * dx / dist)))
this.y = this.y + (( mouse.y - mouse.py) * Math.abs( Math.cos( twoPi * dy / dist)))
}
}
return this
}
}
/*////////////////////////////////////////*/
let count = 0;
class Cloth {
constructor (clothX, clothY, free) {
this.points = []
let startX = 0; //renderer.view.width / 2 - clothX * spacingX / 2;
let startY = 0//renderer.view.height * 0.1;
for (let y = 0; y <= clothY; y++) {
for (let x = 0; x <= clothX; x++) {
let point = new Point(startX + x * spacingX, startY + y * spacingY)
this.points.push(point)
}
}
}
randomize(callback){
this.points.forEach((point,i) => {
point.randomize( i === 0 ? callback : null );
})
}
reset(){
this.points.forEach((point) => {
point.reset()
})
}
update (delta) {
this.points.forEach((point,i) => {
point.update(delta * delta)
if ( mesh ) {
i *= 2;
mesh.vertices[i] = point.x;
mesh.vertices[i+1] = point.y;
}
});
}
}
function pointerMove(e) {
let pointer = e.touches ? e.touches[0] : e;
mouse.px = mouse.x || pointer.clientX
mouse.py = mouse.y || pointer.clientY
mouse.x = pointer.clientX
mouse.y = pointer.clientY
}
function pointerDown(e){
mouse.down = true
mouse.button = 1
pointerMove(e);
}
function pointerUp(e){
mouse.down = false;
mouse.px = null;
mouse.py = null;
}
renderer.view.addEventListener('mousedown', pointerDown);
renderer.view.addEventListener('touchstart', pointerDown);
document.body.addEventListener('mousemove',pointerMove);
document.body.addEventListener('touchmove', pointerMove);
document.body.addEventListener('mouseup', pointerUp);
document.body.addEventListener('touchend', pointerUp);
document.body.addEventListener('mouseleave', pointerUp);
console.clear();
let mesh;
let cloth;
let spacingX = 5;
let spacingY = 5;
let opts = {
image: 'https://unsplash.it/400/400?image=1074',
pointsX: 40,
pointsY: 40,
brushSize: 30,
randomImage(){
this.image = 'https://unsplash.it/400/400?image=' + Math.floor(Math.random() * 1100);
loadTexture();
},
reset(){
if ( cloth ) { cloth.reset(); }
},
randomizePoints(){
if ( cloth ) { cloth.randomize(); }
}
};
/*////////////////////////////////////////*/
let gui = new dat.GUI();
gui.closed = window.innerWidth < 600;
let image = gui.add(opts, 'image', {
Face: 'http://brokensquare.com/Code/assets/face.png',
Lion: 'https://unsplash.it/400/400?image=1074',
Water: 'https://unsplash.it/400/400?image=1053',
YellowCurtain: 'https://unsplash.it/400/400?image=855',
Tunnel: 'https://unsplash.it/400/400?image=137'
});
image.onChange(loadTexture);
let random = gui.add(opts, 'randomImage');
let randomize = gui.add(opts, 'randomizePoints');
let reset = gui.add(opts, 'reset');
/*////////////////////////////////////////*/
let mouse = {
down: false,
x: 0,
y: 0,
px: 0,
py: 1
}
let brush = new PIXI.Graphics();
function updateBrush(){
brush.clear();
brush.blendMode = PIXI.BLEND_MODES.ADD;
brush.lineStyle(1, 0x888888, 0.4);
brush.drawCircle(0, 0, opts.brushSize); // drawCircle(x, y, radius)
brush.x = mouse.x;
brush.y = mouse.y;
brush.updateLocalBounds();
}
updateBrush();
let influence = gui.add(opts, 'brushSize', 0, 100).step(1);
influence.onChange(updateBrush);
/*////////////////////////////////////////*/
let stage = new PIXI.Container();
stage.addChild(brush);
let renderer = PIXI.autoDetectRenderer(window.innerWidth, window.innerHeight, { transparent: true });
document.body.appendChild(renderer.view);
renderer.render(stage);
/*////////////////////////////////////////*/
function loadTexture() {
console.log('loading texture', opts.image);
document.body.className = 'loading';
let texture = new PIXI.Texture.fromImage(opts.image);
if ( !texture.requiresUpdate ) { texture.update(); }
texture.on('error', function(){ console.error('AGH!'); });
texture.on('update',function(){
document.body.className = '';
console.log('texture loaded');
if ( mesh ) { stage.removeChild(mesh); }
mesh = new PIXI.mesh.Plane( this, opts.pointsX, opts.pointsY);
mesh.width = this.width;
mesh.height = this.height;
mesh.x = renderer.width / 2 - mesh.width / 2;
mesh.y = renderer.height / 2 - mesh.height / 2;
spacingX = mesh.width / (opts.pointsX-1);
spacingY = mesh.height / (opts.pointsY-1);
cloth = new Cloth(opts.pointsX-1, opts.pointsY-1, !opts.pinCorners);
stage.addChildAt(mesh,0);
});
}
loadTexture(opts.image);
/*////////////////////////////////////////*/
;(function update() {
requestAnimationFrame(update);
if ( cloth ) { cloth.update(0.016) }
brush.x = mouse.x;
brush.y = mouse.y;
renderer.render(stage);
})(0)
/*////////////////////////////////////////*/
const twoPi = Math.PI * 2;
class Point {
constructor (x, y) {
this.x = this.origX = x
this.y = this.origY = y
}
animateTo(nx, ny, force){
if ( !this.resetting || force ) {
let dx = nx - this.x
let dy = ny - this.y
let dist = Math.sqrt(dx * dx + dy * dy)
this.resetting = true;
TweenMax.to(this, Math.max(0.5,dist / 30),{
x: nx,
y: ny,
ease: Elastic.easeOut,
onComplete: () => {
this.resetting = false
}
})
}
}
randomize() {
let nx = this.x + ((Math.random() * 60) - 30);
let ny = this.y + ((Math.random() * 60) - 30);
this.animateTo(nx, ny);
}
reset(){
this.animateTo(this.origX, this.origY, true);
}
update (delta) {
let dx;
let dy;
if (!this.resetting && mouse.down) {
dx = this.x - mouse.x + mesh.x
dy = this.y - mouse.y + mesh.y
let dist = Math.sqrt(dx * dx + dy * dy)
if ( dist < opts.brushSize) {
this.x = this.x + (( mouse.x - mouse.px) * Math.abs( Math.cos( twoPi * dx / dist)))
this.y = this.y + (( mouse.y - mouse.py) * Math.abs( Math.cos( twoPi * dy / dist)))
}
}
// if ( this.resetting ) {
// dx = this.origX - this.x;
// this.x = this.x + ( dx ) * 0.3;
// dy = this.origY - this.y;
// this.y = this.y + ( dy ) * 0.3;
// if ( Math.abs(dx) + Math.abs(dy) < 0.01 ) {
// this.x = this.origX;
// this.y = this.origY;
// this.resetting = false;
// }
// }
return this
}
}
/*////////////////////////////////////////*/
let count = 0;
class Cloth {
constructor (clothX, clothY, free) {
this.points = []
let startX = 0; //renderer.view.width / 2 - clothX * spacingX / 2;
let startY = 0//renderer.view.height * 0.1;
for (let y = 0; y <= clothY; y++) {
for (let x = 0; x <= clothX; x++) {
let point = new Point(startX + x * spacingX, startY + y * spacingY)
this.points.push(point)
}
}
}
randomize(){
this.points.forEach((point) => {
point.randomize();
})
}
reset(){
this.points.forEach((point) => {
point.reset()
})
}
update (delta) {
this.points.forEach((point,i) => {
point.update(delta * delta)
if ( mesh ) {
i *= 2;
mesh.vertices[i] = point.x;
mesh.vertices[i+1] = point.y;
}
});
}
}
function pointerMove(e) {
let pointer = e.touches ? e.touches[0] : e;
mouse.px = mouse.x || pointer.clientX
mouse.py = mouse.y || pointer.clientY
mouse.x = pointer.clientX
mouse.y = pointer.clientY
}
function pointerDown(e){
mouse.down = true
mouse.button = 1
pointerMove(e);
}
function pointerUp(e){
mouse.down = false;
mouse.px = null;
mouse.py = null;
}
renderer.view.addEventListener('mousedown', pointerDown);
renderer.view.addEventListener('touchstart', pointerDown);
document.body.addEventListener('mousemove',pointerMove);
document.body.addEventListener('touchmove', pointerMove);
document.body.addEventListener('mouseup', pointerUp);
document.body.addEventListener('touchend', pointerUp);
document.body.addEventListener('mouseleave', pointerUp);
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.0.3/pixi.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.1/dat.gui.min.js"></script>
<script src="http://codepen.io/shshaw/pen/epmrgO"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.0/TweenMax.min.js"></script>
canvas { position: absolute; top: 0; left: 0; z-index: -1; }
canvas + canvas { opacity: 0.3; z-index: -2; }
.desc { position: absolute; z-index: 10; left: 0; right: 0; bottom: 0; padding: 1em; font-size: 10px; color: #aaa; text-align: center; }
.loading:before {
position: absolute;
top: 0;
left: 0;
padding: 1em;
content: 'Loading...';
color: #aaa;
font-family: monospace;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment