Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Water-like displacement effect #42

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### 0.x.x (2024-xx-xx)

_New:_

- Extended `displacement` effect to support blue channel for intensity.
- Added `multi-pointer` demo.

### 0.10.2 (2024-08-25)

_Fixed:_
Expand Down
11 changes: 10 additions & 1 deletion demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,18 @@ const sectionScripts = {
refresh: 'refresh9',
});
},

section10() {
startDemo('./multi-pointer.js', {
code: 'code10',
preview: 'preview',
refresh: 'refresh10',
});
},
};

insertSection('section7');
insertSection('section10');


// on load get active section from url hash and set
const hash = window.location.hash.substring(1);
Expand Down
16 changes: 16 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,17 @@
</section>
</template>

<template id="section10">
<section id="multi-pointer">
<div class="code-container">
<button class="refresh-button" id="refresh10">
Refresh
</button>
<textarea id="code10"></textarea>
</div>
</section>
</template>

<nav>
<ul>
<li>
Expand Down Expand Up @@ -360,6 +371,11 @@
>Luminance Mask</a
>
</li>
<li>
<a href="#section10" data-section-id="section10"
>Water-like Displacement</a
>
</li>
</ul>
</nav>
<main>
Expand Down
11 changes: 10 additions & 1 deletion demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,18 @@
refresh: 'refresh9',
});
},

section10() {
startDemo('./multi-pointer.js', {
code: 'code10',
preview: 'preview',
refresh: 'refresh10',
});
},
};

insertSection('section7');
insertSection('section10');


// on load get active section from url hash and set
const hash = window.location.hash.substring(1);
Expand Down
166 changes: 166 additions & 0 deletions demo/multi-pointer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Kampos, effects } from '../index.js';

/*!
* Adapted from Daniel Velasquez's "Creating a Water-like Distortion with Three.js"
* https://tympanus.net/codrops/2019/10/08/creating-a-water-like-distortion-effect-with-three-js/
*/

const DEBUG = false;

const target = document.querySelector('#target');
const mapTarget = document.createElement('canvas');

const easeOutSine = (t) => {
return Math.sin(t * Math.PI / 2);
};

const easeOutQuad = (t) => {
return -1 * t * (t - 2) ;
};

class PointerTexture {
constructor ({ target, width, height, radius, intensity, maxAge, canvas, forceDecay }){
this.width = width;
this.height = height;
this.radius = radius || 100;
this.intensity = intensity;
this.maxAge = maxAge;
this.decayFactor = forceDecay || 0.01;
this.last = null;
this.points = [];
this.ctx = canvas.getContext('2d');

target.addEventListener('pointermove', this.addPoint.bind(this));
}

clear() {
this.ctx.fillStyle = 'rgba(127, 127, 0, 1)';
this.ctx.fillRect(0, 0, this.width, this.height);
}

addPoint (e){
const point = {
x: e.offsetX / this.width,
y: e.offsetY / this.height,
age: 0,
};

if (this.last) {
const relatveX = point.x - this.last.x;
const relatveY = point.y - this.last.y;

const speedSquared = relatveX ** 2 + relatveY ** 2;
const speed = Math.sqrt(speedSquared);

point.vx = relatveX / (speed + 1e-5);
point.vy = relatveY / (speed + 1e-5);

point.speed = Math.min(speedSquared * 1e3, 1);
}

this.last = point;

this.points.push(point);
}

update () {
this.clear();
this.points.forEach((point, i) => {
const decay = 1 - (point.age / this.maxAge);
const force = point.speed * decay * this.decayFactor;
point.x += point.vx * force;
point.y += point.vy * force;
point.age += 1;

if (point.age > this.maxAge) {
this.points.splice(i, 1);
} else {
this.drawPoint(point);
}
});
}

drawPoint (point) {
const position = {
x: point.x * this.width,
y: point.y * this.height
};
let intensity = 1;
if (point.age < this.maxAge * 0.3) {
intensity = easeOutSine(point.age / (this.maxAge * 0.3));
} else {
intensity = easeOutQuad(1 - (point.age - this.maxAge * 0.3) / (this.maxAge * 0.7));
}
intensity *= point.speed;

const red = (1 + point.vx) / 2 * 255;
const green = (1 + point.vy) / 2 * 255;
const blue = intensity * 255;

const offset = this.width * 5;
this.ctx.shadowOffsetX = offset;
this.ctx.shadowOffsetY = offset;
this.ctx.shadowBlur = this.radius;
this.ctx.shadowColor = `rgba(${red}, ${green}, ${blue}, ${this.intensity * intensity})`;

this.ctx.beginPath();
this.ctx.fillStyle = 'rgba(255, 0 , 0, 0, 1)';
this.ctx.arc(position.x - offset, position.y - offset, this.radius, 0, 2 * Math.PI);
this.ctx.fill();
}
}

const MAP_WIDTH = 1396;
const MAP_HEIGHT = 992;

loadImage(
`https://picsum.photos/${MAP_WIDTH}/${MAP_HEIGHT}?random=1`,
).then((img) => {
const height = window.innerHeight;
const width = (height * img.naturalWidth) / img.naturalHeight;

target.width = width;
target.height = height;

mapTarget.width = width;
mapTarget.height = height;

// const render = new Kampos({ target: mapTarget, effects: [pointer], noSource: true });
const pointerTexture = new PointerTexture({
target,
width,
height,
canvas: DEBUG ? target : mapTarget,
radius: 130,
intensity: 1.2,
maxAge: 130,
forceDecay: 0.01,
});

// create the main instance that renders the displaced image
const displacement = effects.displacement({
scale: { x: 0, y: 0 },
enableBlueChannel: true,
});
displacement.map = mapTarget;
displacement.textures[0].update = true; // to update

if (DEBUG) {
function tick () {
requestAnimationFrame(tick);

pointerTexture.update();
}

requestAnimationFrame(tick);
} else {
const instance = new Kampos({target, effects: [displacement]});

// // set media source
instance.setSource({media: img, width, height});

instance.play(() => {
pointerTexture.update();
});
}
});
21 changes: 18 additions & 3 deletions dist/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -767,18 +767,20 @@ function duotone ({
* @param {Object} [params]
* @param {string} [params.wrap] wrapping method to use. Defaults to `displacement.CLAMP`.
* @param {{x: number, y: number}} [params.scale] initial scale to use for x and y displacement. Defaults to `{x: 0.0, y: 0.0}` which means no displacement.
* @param {boolean} [params.enableBlueChannel] enable blue channel for displacement intensity. Defaults to `false`.
* @returns {displacementEffect}
*
* @example displacement({wrap: displacement.DISCARD, scale: {x: 0.5, y: -0.5}})
*/
function displacement({ wrap = WRAP_METHODS.CLAMP, scale } = {}) {
function displacement({ wrap = WRAP_METHODS.CLAMP, scale, enableBlueChannel } = {}) {
const { x: sx, y: sy } = scale || { x: 0.0, y: 0.0 };

/**
* @typedef {Object} displacementEffect
* @property {ArrayBufferView|ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|ImageBitmap} map
* @property {{x: number?, y: number?}} scale
* @property {boolean} disabled
* @property {boolean} enableBlueChannel
*
* @example
* const img = new Image();
Expand All @@ -797,13 +799,15 @@ function displacement({ wrap = WRAP_METHODS.CLAMP, scale } = {}) {
fragment: {
uniform: {
u_displacementEnabled: 'bool',
u_enableBlueChannel: 'bool',
u_dispMap: 'sampler2D',
u_dispScale: 'vec2',
},
source: `
if (u_displacementEnabled) {
vec3 dispMap = texture2D(u_dispMap, v_displacementMapTexCoord).rgb - 0.5;
vec2 dispVec = vec2(sourceCoord.x + u_dispScale.x * dispMap.r, sourceCoord.y + u_dispScale.y * dispMap.g);
float dispMapB = u_enableBlueChannel ? dispMap.b : 0.0;
vec2 dispVec = vec2(sourceCoord.x + (u_dispScale.x + dispMapB) * dispMap.r, sourceCoord.y + (u_dispScale.y + dispMapB) * dispMap.g);
${wrap}
sourceCoord = dispVec;
}`,
Expand All @@ -828,6 +832,12 @@ function displacement({ wrap = WRAP_METHODS.CLAMP, scale } = {}) {
set map(img) {
this.textures[0].data = img;
},
get enableBlueChannel() {
return this.uniforms[3].data[0];
},
set enableBlueChannel(b) {
this.uniforms[3].data[0] = +b;
},
varying: {
v_displacementMapTexCoord: 'vec2',
},
Expand All @@ -847,6 +857,11 @@ function displacement({ wrap = WRAP_METHODS.CLAMP, scale } = {}) {
type: 'f',
data: [sx, sy],
},
{
name: 'u_enableBlueChannel',
type: 'i',
data: [+enableBlueChannel],
},
],
attributes: [
{
Expand Down Expand Up @@ -3231,7 +3246,7 @@ const effects = {
duotone,
displacement,
turbulence,
kaleidoscope
kaleidoscope,
};

const transitions = {
Expand Down
17 changes: 17 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2711,6 +2711,17 @@ <h3 class='fl m0' id='effects'>



<tr>
<td class='break-word'><span class='code bold'>params.enableBlueChannel</span> <code class='quiet'><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>?</code>
</td>
<td class='break-word'><span>enable blue channel for displacement intensity. Defaults to
<code>false</code>
.
</span></td>
</tr>



</tbody>
</table>

Expand Down Expand Up @@ -3579,6 +3590,12 @@ <h3 class='fl m0' id='displacementeffect'>

</div>

<div class='space-bottom0'>
<span class='code bold'>enableBlueChannel</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>)</code>


</div>

</div>


Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const effects = {
duotone,
displacement,
turbulence,
kaleidoscope
kaleidoscope,
};

export const transitions = {
Expand Down
Loading
Loading