react-sea-motion
Version:
A React component that adds fluid, sea-like motion effects to images
352 lines (341 loc) • 20.3 kB
JavaScript
import { jsx, jsxs } from 'react/jsx-runtime';
import { useRef, useState, useEffect, useCallback } from 'react';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var SeaMotion = function (_a) {
var src = _a.src, _b = _a.alt, alt = _b === void 0 ? '' : _b, _c = _a.className, className = _c === void 0 ? '' : _c, _d = _a.style, style = _d === void 0 ? {} : _d, _e = _a.speed, speed = _e === void 0 ? 0.3 : _e, _f = _a.intensity, intensity = _f === void 0 ? 1.0 : _f, duration = _a.duration, // undefined = infinite by default
children = _a.children, onLoad = _a.onLoad, onError = _a.onError, onAnimationEnd = _a.onAnimationEnd;
var canvasRef = useRef(null);
var containerRef = useRef(null);
var animationRef = useRef();
var programRef = useRef(null);
var textureRef = useRef(null);
var glRef = useRef(null);
var startTimeRef = useRef(Date.now());
var timerRef = useRef(null);
var onAnimationEndRef = useRef(onAnimationEnd);
var _g = useState(false), isLoaded = _g[0], setIsLoaded = _g[1];
var _h = useState(null), error = _h[0], setError = _h[1];
var _j = useState(true), isAnimating = _j[0], setIsAnimating = _j[1];
var _k = useState(false), isStoppedByTimer = _k[0], setIsStoppedByTimer = _k[1];
// Update the ref when onAnimationEnd changes
useEffect(function () {
onAnimationEndRef.current = onAnimationEnd;
}, [onAnimationEnd]);
var vertexShaderSource = "\n attribute vec2 a_position;\n attribute vec2 a_texCoord;\n varying vec2 v_texCoord;\n \n void main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texCoord = a_texCoord;\n }\n ";
var fragmentShaderSource = "\n precision mediump float;\n uniform sampler2D u_texture;\n uniform float u_time;\n uniform float u_speed;\n uniform float u_intensity;\n uniform vec2 u_resolution;\n uniform float u_imageAspect;\n uniform float u_canvasAspect;\n varying vec2 v_texCoord;\n \n float noise(vec2 p) {\n return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);\n }\n \n float smoothNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n \n float a = noise(i);\n float b = noise(i + vec2(1.0, 0.0));\n float c = noise(i + vec2(0.0, 1.0));\n float d = noise(i + vec2(1.0, 1.0));\n \n return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n }\n \n float fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n float frequency = 1.0;\n \n for(int i = 0; i < 4; i++) {\n value += amplitude * smoothNoise(p * frequency);\n amplitude *= 0.5;\n frequency *= 2.0;\n }\n return value;\n }\n \n void main() {\n vec2 uv = v_texCoord;\n float time = u_time * u_speed * 0.0003;\n \n float imgAspect = u_imageAspect;\n float canAspect = u_canvasAspect;\n vec2 coverUV = uv;\n float scaleX = 1.0;\n float scaleY = 1.0;\n if (canAspect > imgAspect) {\n scaleY = imgAspect / canAspect;\n coverUV.y = (uv.y - 0.5) * scaleY + 0.5;\n } else {\n scaleX = canAspect / imgAspect;\n coverUV.x = (uv.x - 0.5) * scaleX + 0.5;\n }\n \n float wave1 = sin(coverUV.x * 6.0 + time * 0.8) * 0.02 * u_intensity;\n float wave2 = sin(coverUV.y * 4.0 + time * 0.6) * 0.015 * u_intensity;\n float wave3 = sin((coverUV.x + coverUV.y) * 8.0 + time * 1.2) * 0.01 * u_intensity;\n \n vec2 noisePos = coverUV * 3.0 + time * 0.2;\n float turbulence = fbm(noisePos) * 0.03 * u_intensity;\n \n vec2 center = vec2(0.5, 0.5);\n float dist = length(coverUV - center);\n float ripple = sin(dist * 20.0 - time * 1.5) * 0.008 * (1.0 - dist) * u_intensity;\n \n vec2 distortion = vec2(\n wave1 + wave3 + turbulence + ripple,\n wave2 + wave3 + turbulence * 0.7 + ripple\n );\n \n vec2 distortedUV = coverUV + distortion;\n vec4 color = texture2D(u_texture, distortedUV);\n \n color.rgb += sin(time + coverUV.x * 10.0) * 0.05 * u_intensity;\n color.rgb *= 1.0 + sin(time * 0.8 + dist * 15.0) * 0.1 * u_intensity;\n \n gl_FragColor = color;\n }\n ";
var createShader = useCallback(function (gl, type, source) {
var shader = gl.createShader(type);
if (!shader)
return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
var error_1 = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error("Shader compilation error: ".concat(error_1));
}
return shader;
}, []);
var createProgram = useCallback(function (gl, vertexShader, fragmentShader) {
var program = gl.createProgram();
if (!program)
return null;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
var error_2 = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error("Program linking error: ".concat(error_2));
}
return program;
}, []);
var initWebGL = useCallback(function () {
var canvas = canvasRef.current;
var gl = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('webgl');
if (!gl) {
throw new Error('WebGL not supported');
}
glRef.current = gl;
try {
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
if (!vertexShader || !fragmentShader) {
throw new Error('Failed to create shaders');
}
var program = createProgram(gl, vertexShader, fragmentShader);
if (!program) {
throw new Error('Failed to create program');
}
programRef.current = program;
var positions = new Float32Array([
-1, -1, 0, 1,
1, -1, 1, 1,
-1, 1, 0, 0,
1, 1, 1, 0
]);
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
var positionLocation = gl.getAttribLocation(program, 'a_position');
var texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 16, 0);
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 16, 8);
gl.clearColor(0.0, 0.0, 0.0, 0.0);
}
catch (err) {
throw err;
}
}, [createShader, createProgram]);
var loadImage = useCallback(function (imageSrc) {
return new Promise(function (resolve, reject) {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () { return resolve(img); };
img.onerror = function () { return reject(new Error('Failed to load image')); };
img.src = imageSrc;
});
}, []);
var createTexture = useCallback(function (image) {
var gl = glRef.current;
if (!gl)
return null;
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
return texture;
}, []);
var resizeCanvas = useCallback(function () {
var canvas = canvasRef.current;
var container = containerRef.current;
var gl = glRef.current;
if (!canvas || !container || !gl)
return;
var containerRect = container.getBoundingClientRect();
var containerWidth = Math.max(1, containerRect.width);
var containerHeight = Math.max(1, containerRect.height);
canvas.width = containerWidth;
canvas.height = containerHeight;
canvas.style.width = "".concat(containerWidth, "px");
canvas.style.height = "".concat(containerHeight, "px");
gl.viewport(0, 0, containerWidth, containerHeight);
}, []);
useCallback(function () {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
animationRef.current = undefined;
}
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
setIsAnimating(false);
}, []);
var initializeEffect = useCallback(function (imageSrc) { return __awaiter(void 0, void 0, void 0, function () {
var image, texture, err_1, error_3;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
setError(null);
initWebGL();
return [4 /*yield*/, loadImage(imageSrc)];
case 1:
image = _a.sent();
window._seaMotionImageAspect = image.width / image.height;
resizeCanvas();
texture = createTexture(image);
if (!texture) {
throw new Error('Failed to create texture');
}
textureRef.current = texture;
startTimeRef.current = Date.now();
setIsLoaded(true);
setIsAnimating(true);
setIsStoppedByTimer(false);
onLoad === null || onLoad === void 0 ? void 0 : onLoad();
// Set up timer if duration is specified
if (duration && duration > 0) {
timerRef.current = window.setTimeout(function () {
var _a;
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
animationRef.current = undefined;
}
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
setIsAnimating(false);
setIsStoppedByTimer(true);
(_a = onAnimationEndRef.current) === null || _a === void 0 ? void 0 : _a.call(onAnimationEndRef);
}, duration * 1000); // Convert seconds to milliseconds
}
return [3 /*break*/, 3];
case 2:
err_1 = _a.sent();
error_3 = err_1 instanceof Error ? err_1 : new Error('Unknown error');
setError(error_3.message);
onError === null || onError === void 0 ? void 0 : onError(error_3);
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
}); }, [initWebGL, loadImage, resizeCanvas, createTexture, onLoad, onError, duration]);
useEffect(function () {
if (src) {
initializeEffect(src);
}
return function () {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [src, initializeEffect]);
// Only restart animation if not stopped by timer
useEffect(function () {
if (isLoaded && textureRef.current && isAnimating && !isStoppedByTimer) {
// Clear existing animation and timer
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
// Reset animation start time
startTimeRef.current = Date.now();
// Set up new timer if duration is specified
if (duration && duration > 0) {
timerRef.current = window.setTimeout(function () {
var _a;
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
animationRef.current = undefined;
}
setIsAnimating(false);
setIsStoppedByTimer(true);
(_a = onAnimationEndRef.current) === null || _a === void 0 ? void 0 : _a.call(onAnimationEndRef);
}, duration * 1000);
}
// Start render loop
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
var startRender_1 = function () {
var gl = glRef.current;
var program = programRef.current;
var texture = textureRef.current;
var canvas = canvasRef.current;
var imageAspect = window._seaMotionImageAspect || 1.0;
if (!gl || !program || !texture || !canvas || !isAnimating)
return;
var currentTime = Date.now() - startTimeRef.current;
var canvasAspect = canvas.width / canvas.height;
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
var textureLocation = gl.getUniformLocation(program, 'u_texture');
var timeLocation = gl.getUniformLocation(program, 'u_time');
var speedLocation = gl.getUniformLocation(program, 'u_speed');
var intensityLocation = gl.getUniformLocation(program, 'u_intensity');
var resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
var imageAspectLocation = gl.getUniformLocation(program, 'u_imageAspect');
var canvasAspectLocation = gl.getUniformLocation(program, 'u_canvasAspect');
gl.uniform1i(textureLocation, 0);
gl.uniform1f(timeLocation, currentTime);
gl.uniform1f(speedLocation, speed);
gl.uniform1f(intensityLocation, intensity);
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
gl.uniform1f(imageAspectLocation, imageAspect);
gl.uniform1f(canvasAspectLocation, canvasAspect);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
animationRef.current = requestAnimationFrame(startRender_1);
};
startRender_1();
}
}, [speed, intensity, duration, isLoaded, isAnimating, isStoppedByTimer]);
var containerStyle = __assign({ position: 'relative', display: 'inline-block' }, style);
var canvasStyle = {
display: 'block',
width: '100%',
height: '100%'
};
if (error) {
return (jsx("div", __assign({ className: className, style: containerStyle }, { children: jsxs("div", __assign({ style: { color: 'red', padding: '20px' } }, { children: ["Error: ", error] })) })));
}
return (jsxs("div", __assign({ ref: containerRef, className: className, style: containerStyle }, { children: [jsx("canvas", { ref: canvasRef, style: canvasStyle, "aria-label": alt }), children] })));
};
export { SeaMotion as default };