Add fluid particle animation system with Rust/WASM
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
- Implement 800+ flowing green particles with fluid motion dynamics - Add flow field physics using sine waves for organic movement - Particles feature trails, glow effects, and smooth edge wrapping - Canvas 2D rendering from Rust/WASM for 60fps performance - Fullscreen background animation with automatic window resize - Green gradient colors (emerald to lime, HSL 120-160) - Optimized with semi-transparent clearing for fluid trail effects Inspired by WebAssembly particle demos and WebGPU fluid simulations
This commit is contained in:
parent
543ff246d5
commit
710f48de69
|
|
@ -27,6 +27,9 @@ features = [
|
||||||
"DomRect",
|
"DomRect",
|
||||||
"NodeList",
|
"NodeList",
|
||||||
"DomTokenList",
|
"DomTokenList",
|
||||||
|
"HtmlCanvasElement",
|
||||||
|
"CanvasRenderingContext2d",
|
||||||
|
"Performance",
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface InitOutput {
|
||||||
readonly __wbindgen_export_2: (a: number, b: number, c: number, d: number) => number;
|
readonly __wbindgen_export_2: (a: number, b: number, c: number, d: number) => number;
|
||||||
readonly __wbindgen_export_3: WebAssembly.Table;
|
readonly __wbindgen_export_3: WebAssembly.Table;
|
||||||
readonly __wbindgen_export_4: (a: number, b: number, c: number, d: number) => void;
|
readonly __wbindgen_export_4: (a: number, b: number, c: number, d: number) => void;
|
||||||
|
readonly __wbindgen_export_5: (a: number, b: number) => void;
|
||||||
readonly __wbindgen_start: () => void;
|
readonly __wbindgen_start: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,71 @@ function getDataViewMemory0() {
|
||||||
return cachedDataViewMemory0;
|
return cachedDataViewMemory0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function debugString(val) {
|
||||||
|
// primitive types
|
||||||
|
const type = typeof val;
|
||||||
|
if (type == 'number' || type == 'boolean' || val == null) {
|
||||||
|
return `${val}`;
|
||||||
|
}
|
||||||
|
if (type == 'string') {
|
||||||
|
return `"${val}"`;
|
||||||
|
}
|
||||||
|
if (type == 'symbol') {
|
||||||
|
const description = val.description;
|
||||||
|
if (description == null) {
|
||||||
|
return 'Symbol';
|
||||||
|
} else {
|
||||||
|
return `Symbol(${description})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == 'function') {
|
||||||
|
const name = val.name;
|
||||||
|
if (typeof name == 'string' && name.length > 0) {
|
||||||
|
return `Function(${name})`;
|
||||||
|
} else {
|
||||||
|
return 'Function';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// objects
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const length = val.length;
|
||||||
|
let debug = '[';
|
||||||
|
if (length > 0) {
|
||||||
|
debug += debugString(val[0]);
|
||||||
|
}
|
||||||
|
for(let i = 1; i < length; i++) {
|
||||||
|
debug += ', ' + debugString(val[i]);
|
||||||
|
}
|
||||||
|
debug += ']';
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
// Test for built-in
|
||||||
|
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||||
|
let className;
|
||||||
|
if (builtInMatches && builtInMatches.length > 1) {
|
||||||
|
className = builtInMatches[1];
|
||||||
|
} else {
|
||||||
|
// Failed to match the standard '[object ClassName]'
|
||||||
|
return toString.call(val);
|
||||||
|
}
|
||||||
|
if (className == 'Object') {
|
||||||
|
// we're a user defined class or Object
|
||||||
|
// JSON.stringify avoids problems with cycles, and is generally much
|
||||||
|
// easier than looping through ownProperties of `val`.
|
||||||
|
try {
|
||||||
|
return 'Object(' + JSON.stringify(val) + ')';
|
||||||
|
} catch (_) {
|
||||||
|
return 'Object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// errors
|
||||||
|
if (val instanceof Error) {
|
||||||
|
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||||
|
}
|
||||||
|
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
function dropObject(idx) {
|
function dropObject(idx) {
|
||||||
if (idx < 132) return;
|
if (idx < 132) return;
|
||||||
heap[idx] = heap_next;
|
heap[idx] = heap_next;
|
||||||
|
|
@ -176,6 +241,10 @@ function __wbg_adapter_4(arg0, arg1, arg2, arg3) {
|
||||||
wasm.__wbindgen_export_4(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
wasm.__wbindgen_export_4(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_9(arg0, arg1) {
|
||||||
|
wasm.__wbindgen_export_5(arg0, arg1);
|
||||||
|
}
|
||||||
|
|
||||||
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
|
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
|
||||||
|
|
||||||
async function __wbg_load(module, imports) {
|
async function __wbg_load(module, imports) {
|
||||||
|
|
@ -214,6 +283,9 @@ async function __wbg_load(module, imports) {
|
||||||
function __wbg_get_imports() {
|
function __wbg_get_imports() {
|
||||||
const imports = {};
|
const imports = {};
|
||||||
imports.wbg = {};
|
imports.wbg = {};
|
||||||
|
imports.wbg.__wbg_addEventListener_775911544ac9d643 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
|
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3));
|
||||||
|
}, arguments) };
|
||||||
imports.wbg.__wbg_add_4e0283c00f7ecabe = function() { return handleError(function (arg0, arg1, arg2) {
|
imports.wbg.__wbg_add_4e0283c00f7ecabe = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
|
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
|
|
@ -221,6 +293,12 @@ function __wbg_get_imports() {
|
||||||
const ret = getObject(arg0).appendChild(getObject(arg1));
|
const ret = getObject(arg0).appendChild(getObject(arg1));
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_arc_61cbec33cc96a55e = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
|
||||||
|
getObject(arg0).arc(arg1, arg2, arg3, arg4, arg5);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_beginPath_119487ebd04e9e1c = function(arg0) {
|
||||||
|
getObject(arg0).beginPath();
|
||||||
|
};
|
||||||
imports.wbg.__wbg_body_8822ca55cb3730d2 = function(arg0) {
|
imports.wbg.__wbg_body_8822ca55cb3730d2 = function(arg0) {
|
||||||
const ret = getObject(arg0).body;
|
const ret = getObject(arg0).body;
|
||||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
|
|
@ -241,6 +319,12 @@ function __wbg_get_imports() {
|
||||||
const ret = getObject(arg0).document;
|
const ret = getObject(arg0).document;
|
||||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_fillRect_a160edfa11fce49b = function(arg0, arg1, arg2, arg3, arg4) {
|
||||||
|
getObject(arg0).fillRect(arg1, arg2, arg3, arg4);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_fill_7b331ac62ac7c50b = function(arg0) {
|
||||||
|
getObject(arg0).fill();
|
||||||
|
};
|
||||||
imports.wbg.__wbg_getAttribute_8bfaf67e99ed2ee3 = function(arg0, arg1, arg2, arg3) {
|
imports.wbg.__wbg_getAttribute_8bfaf67e99ed2ee3 = function(arg0, arg1, arg2, arg3) {
|
||||||
const ret = getObject(arg1).getAttribute(getStringFromWasm0(arg2, arg3));
|
const ret = getObject(arg1).getAttribute(getStringFromWasm0(arg2, arg3));
|
||||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_1, wasm.__wbindgen_export_2);
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_1, wasm.__wbindgen_export_2);
|
||||||
|
|
@ -248,10 +332,32 @@ function __wbg_get_imports() {
|
||||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_getContext_15e158d04230a6f6 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2));
|
||||||
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
imports.wbg.__wbg_get_0da715ceaecea5c8 = function(arg0, arg1) {
|
imports.wbg.__wbg_get_0da715ceaecea5c8 = function(arg0, arg1) {
|
||||||
const ret = getObject(arg0)[arg1 >>> 0];
|
const ret = getObject(arg0)[arg1 >>> 0];
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_innerHeight_eacbddff807274db = function() { return handleError(function (arg0) {
|
||||||
|
const ret = getObject(arg0).innerHeight;
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_innerWidth_dd42bfe6b5e91e59 = function() { return handleError(function (arg0) {
|
||||||
|
const ret = getObject(arg0).innerWidth;
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_instanceof_CanvasRenderingContext2d_8c616198ec03b12f = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = getObject(arg0) instanceof CanvasRenderingContext2D;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_instanceof_Element_162e4334c7d6f450 = function(arg0) {
|
imports.wbg.__wbg_instanceof_Element_162e4334c7d6f450 = function(arg0) {
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
|
|
@ -262,6 +368,16 @@ function __wbg_get_imports() {
|
||||||
const ret = result;
|
const ret = result;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_HtmlCanvasElement_299c60950dbb3428 = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = getObject(arg0) instanceof HTMLCanvasElement;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_instanceof_IntersectionObserverEntry_819e56422a481344 = function(arg0) {
|
imports.wbg.__wbg_instanceof_IntersectionObserverEntry_819e56422a481344 = function(arg0) {
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
|
|
@ -320,12 +436,29 @@ function __wbg_get_imports() {
|
||||||
const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2));
|
const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2));
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_random_7ed63a0b38ee3b75 = function() {
|
||||||
|
const ret = Math.random();
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_requestAnimationFrame_ddc84a7def436784 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = getObject(arg0).requestAnimationFrame(getObject(arg1));
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||||
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
imports.wbg.__wbg_setclassName_c8bccad917b973f4 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbg_setclassName_c8bccad917b973f4 = function(arg0, arg1, arg2) {
|
||||||
getObject(arg0).className = getStringFromWasm0(arg1, arg2);
|
getObject(arg0).className = getStringFromWasm0(arg1, arg2);
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_setfillStyle_a9ad5b25cf62a5bc = function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0).fillStyle = getStringFromWasm0(arg1, arg2);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setheight_4fce583024b2d088 = function(arg0, arg1) {
|
||||||
|
getObject(arg0).height = arg1 >>> 0;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setid_891db4c7fb5255d1 = function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0).id = getStringFromWasm0(arg1, arg2);
|
||||||
|
};
|
||||||
imports.wbg.__wbg_setinnerHTML_34e240d6b8e8260c = function(arg0, arg1, arg2) {
|
imports.wbg.__wbg_setinnerHTML_34e240d6b8e8260c = function(arg0, arg1, arg2) {
|
||||||
getObject(arg0).innerHTML = getStringFromWasm0(arg1, arg2);
|
getObject(arg0).innerHTML = getStringFromWasm0(arg1, arg2);
|
||||||
};
|
};
|
||||||
|
|
@ -335,6 +468,9 @@ function __wbg_get_imports() {
|
||||||
imports.wbg.__wbg_setthreshold_7daca4126268ea47 = function(arg0, arg1) {
|
imports.wbg.__wbg_setthreshold_7daca4126268ea47 = function(arg0, arg1) {
|
||||||
getObject(arg0).threshold = getObject(arg1);
|
getObject(arg0).threshold = getObject(arg1);
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_setwidth_40a6ed203b92839d = function(arg0, arg1) {
|
||||||
|
getObject(arg0).width = arg1 >>> 0;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_static_accessor_GLOBAL_8921f820c2ce3f12 = function() {
|
imports.wbg.__wbg_static_accessor_GLOBAL_8921f820c2ce3f12 = function() {
|
||||||
const ret = typeof global === 'undefined' ? null : global;
|
const ret = typeof global === 'undefined' ? null : global;
|
||||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
|
|
@ -364,10 +500,23 @@ function __wbg_get_imports() {
|
||||||
const ret = false;
|
const ret = false;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_wbindgendebugstring_99ef257a3ddda34d = function(arg0, arg1) {
|
||||||
|
const ret = debugString(getObject(arg1));
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export_1, wasm.__wbindgen_export_2);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
};
|
||||||
imports.wbg.__wbg_wbindgenisundefined_c4b71d073b92f3c5 = function(arg0) {
|
imports.wbg.__wbg_wbindgenisundefined_c4b71d073b92f3c5 = function(arg0) {
|
||||||
const ret = getObject(arg0) === undefined;
|
const ret = getObject(arg0) === undefined;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_wbindgennumberget_f74b4c7525ac05cb = function(arg0, arg1) {
|
||||||
|
const obj = getObject(arg1);
|
||||||
|
const ret = typeof(obj) === 'number' ? obj : undefined;
|
||||||
|
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||||
|
};
|
||||||
imports.wbg.__wbg_wbindgenrethrow_01815c9239d70cc2 = function(arg0) {
|
imports.wbg.__wbg_wbindgenrethrow_01815c9239d70cc2 = function(arg0) {
|
||||||
throw takeObject(arg0);
|
throw takeObject(arg0);
|
||||||
};
|
};
|
||||||
|
|
@ -379,13 +528,18 @@ function __wbg_get_imports() {
|
||||||
const ret = getStringFromWasm0(arg0, arg1);
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbindgen_cast_aaa93aae03c115ab = function(arg0, arg1) {
|
||||||
|
// Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_9);
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) {
|
imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) {
|
||||||
// Cast intrinsic for `F64 -> Externref`.
|
// Cast intrinsic for `F64 -> Externref`.
|
||||||
const ret = arg0;
|
const ret = arg0;
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_cast_fe9164a03cdb0b6b = function(arg0, arg1) {
|
imports.wbg.__wbindgen_cast_e58c2e3d55f4d158 = function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [NamedExternref("Array<any>"), NamedExternref("IntersectionObserver")], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [NamedExternref("Array<any>"), NamedExternref("IntersectionObserver")], shim_idx: 4, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_4);
|
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_4);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -7,4 +7,5 @@ export const __wbindgen_export_1: (a: number, b: number) => number;
|
||||||
export const __wbindgen_export_2: (a: number, b: number, c: number, d: number) => number;
|
export const __wbindgen_export_2: (a: number, b: number, c: number, d: number) => number;
|
||||||
export const __wbindgen_export_3: WebAssembly.Table;
|
export const __wbindgen_export_3: WebAssembly.Table;
|
||||||
export const __wbindgen_export_4: (a: number, b: number, c: number, d: number) => void;
|
export const __wbindgen_export_4: (a: number, b: number, c: number, d: number) => void;
|
||||||
|
export const __wbindgen_export_5: (a: number, b: number) => void;
|
||||||
export const __wbindgen_start: () => void;
|
export const __wbindgen_start: () => void;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,17 @@ body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Particle canvas background */
|
||||||
|
#particle-canvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading screen */
|
/* Loading screen */
|
||||||
#loading {
|
#loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -72,9 +83,9 @@ body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero::before {
|
.hero::before {
|
||||||
|
|
@ -130,6 +141,8 @@ body {
|
||||||
/* Section Styles */
|
/* Section Styles */
|
||||||
section {
|
section {
|
||||||
padding: 5rem 0;
|
padding: 5rem 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
|
|
||||||
235
src/lib.rs
235
src/lib.rs
|
|
@ -1,6 +1,154 @@
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{console, Document, Element, HtmlElement, Window, IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit};
|
use web_sys::{
|
||||||
|
console, Document, Element, HtmlElement, Window,
|
||||||
|
IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit,
|
||||||
|
HtmlCanvasElement, CanvasRenderingContext2d,
|
||||||
|
};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
// Particle structure
|
||||||
|
struct Particle {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
vx: f64,
|
||||||
|
vy: f64,
|
||||||
|
size: f64,
|
||||||
|
life: f64,
|
||||||
|
max_life: f64,
|
||||||
|
hue: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Particle {
|
||||||
|
fn new(x: f64, y: f64, hue: f64) -> Self {
|
||||||
|
let angle = js_sys::Math::random() * std::f64::consts::PI * 2.0;
|
||||||
|
let speed = js_sys::Math::random() * 0.5 + 0.2;
|
||||||
|
Self {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
vx: angle.cos() * speed,
|
||||||
|
vy: angle.sin() * speed,
|
||||||
|
size: js_sys::Math::random() * 2.0 + 1.0,
|
||||||
|
life: 1.0,
|
||||||
|
max_life: js_sys::Math::random() * 100.0 + 100.0,
|
||||||
|
hue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, width: f64, height: f64, time: f64) {
|
||||||
|
// Flow field influence using sine waves for fluid motion
|
||||||
|
let flow_x = ((self.y * 0.01 + time * 0.0005).sin() * 0.5).sin();
|
||||||
|
let flow_y = ((self.x * 0.01 + time * 0.0003).cos() * 0.5).cos();
|
||||||
|
|
||||||
|
self.vx += flow_x * 0.1;
|
||||||
|
self.vy += flow_y * 0.1;
|
||||||
|
|
||||||
|
// Damping for smooth motion
|
||||||
|
self.vx *= 0.98;
|
||||||
|
self.vy *= 0.98;
|
||||||
|
|
||||||
|
self.x += self.vx;
|
||||||
|
self.y += self.vy;
|
||||||
|
|
||||||
|
self.life -= 1.0;
|
||||||
|
|
||||||
|
// Wrap around edges with smooth transition
|
||||||
|
if self.x < 0.0 {
|
||||||
|
self.x = width;
|
||||||
|
}
|
||||||
|
if self.x > width {
|
||||||
|
self.x = 0.0;
|
||||||
|
}
|
||||||
|
if self.y < 0.0 {
|
||||||
|
self.y = height;
|
||||||
|
}
|
||||||
|
if self.y > height {
|
||||||
|
self.y = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_dead(&self) -> bool {
|
||||||
|
self.life <= 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, ctx: &CanvasRenderingContext2d) {
|
||||||
|
let alpha = (self.life / self.max_life).min(1.0);
|
||||||
|
let color = format!("hsla({}, 70%, 50%, {})", self.hue, alpha * 0.8);
|
||||||
|
|
||||||
|
ctx.set_fill_style_str(&color);
|
||||||
|
ctx.begin_path();
|
||||||
|
ctx.arc(self.x, self.y, self.size, 0.0, std::f64::consts::PI * 2.0).ok();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Add glow effect
|
||||||
|
let glow_color = format!("hsla({}, 70%, 60%, {})", self.hue, alpha * 0.3);
|
||||||
|
ctx.set_fill_style_str(&glow_color);
|
||||||
|
ctx.begin_path();
|
||||||
|
ctx.arc(self.x, self.y, self.size * 2.0, 0.0, std::f64::consts::PI * 2.0).ok();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Particle system
|
||||||
|
struct ParticleSystem {
|
||||||
|
particles: Vec<Particle>,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
max_particles: usize,
|
||||||
|
time: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParticleSystem {
|
||||||
|
fn new(width: f64, height: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
particles: Vec::new(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
max_particles: 800,
|
||||||
|
time: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_particles(&mut self, count: usize) {
|
||||||
|
for _ in 0..count {
|
||||||
|
if self.particles.len() < self.max_particles {
|
||||||
|
let x = js_sys::Math::random() * self.width;
|
||||||
|
let y = js_sys::Math::random() * self.height;
|
||||||
|
// Green hues: 120-160 (emerald to lime)
|
||||||
|
let hue = js_sys::Math::random() * 40.0 + 120.0;
|
||||||
|
self.particles.push(Particle::new(x, y, hue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self) {
|
||||||
|
self.time += 1.0;
|
||||||
|
|
||||||
|
// Update existing particles
|
||||||
|
for particle in &mut self.particles {
|
||||||
|
particle.update(self.width, self.height, self.time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove dead particles
|
||||||
|
self.particles.retain(|p| !p.is_dead());
|
||||||
|
|
||||||
|
// Spawn new particles
|
||||||
|
let spawn_count = (self.max_particles - self.particles.len()).min(5);
|
||||||
|
self.spawn_particles(spawn_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, ctx: &CanvasRenderingContext2d) {
|
||||||
|
for particle in &self.particles {
|
||||||
|
particle.draw(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize(&mut self, width: f64, height: f64) {
|
||||||
|
self.width = width;
|
||||||
|
self.height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
pub fn main() -> Result<(), JsValue> {
|
pub fn main() -> Result<(), JsValue> {
|
||||||
|
|
@ -12,6 +160,10 @@ pub fn main() -> Result<(), JsValue> {
|
||||||
// Clear body and build page
|
// Clear body and build page
|
||||||
if let Some(body) = document.body() {
|
if let Some(body) = document.body() {
|
||||||
body.set_inner_html("");
|
body.set_inner_html("");
|
||||||
|
|
||||||
|
// Create and setup particle canvas
|
||||||
|
setup_particle_canvas(&document, &body, &window)?;
|
||||||
|
|
||||||
build_page(&document, &body)?;
|
build_page(&document, &body)?;
|
||||||
setup_scroll_animations(&document)?;
|
setup_scroll_animations(&document)?;
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +172,85 @@ pub fn main() -> Result<(), JsValue> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup_particle_canvas(document: &Document, body: &HtmlElement, window: &Window) -> Result<(), JsValue> {
|
||||||
|
// Create canvas element
|
||||||
|
let canvas = document
|
||||||
|
.create_element("canvas")?
|
||||||
|
.dyn_into::<HtmlCanvasElement>()?;
|
||||||
|
canvas.set_id("particle-canvas");
|
||||||
|
|
||||||
|
// Get window dimensions
|
||||||
|
let width = window.inner_width()?.as_f64().unwrap_or(800.0);
|
||||||
|
let height = window.inner_height()?.as_f64().unwrap_or(600.0);
|
||||||
|
|
||||||
|
canvas.set_width(width as u32);
|
||||||
|
canvas.set_height(height as u32);
|
||||||
|
|
||||||
|
body.append_child(&canvas)?;
|
||||||
|
|
||||||
|
// Get 2D context
|
||||||
|
let context = canvas
|
||||||
|
.get_context("2d")?
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<CanvasRenderingContext2d>()?;
|
||||||
|
|
||||||
|
// Initialize particle system
|
||||||
|
let particle_system = Rc::new(RefCell::new(ParticleSystem::new(width, height)));
|
||||||
|
|
||||||
|
// Spawn initial particles
|
||||||
|
particle_system.borrow_mut().spawn_particles(400);
|
||||||
|
|
||||||
|
// Setup animation loop
|
||||||
|
let system_clone = particle_system.clone();
|
||||||
|
let context_clone = context.clone();
|
||||||
|
|
||||||
|
let animate_closure = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
|
||||||
|
let animate_clone = animate_closure.clone();
|
||||||
|
|
||||||
|
*animate_closure.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
||||||
|
// Clear canvas with trail effect (don't fully clear for fluid trails)
|
||||||
|
context_clone.set_fill_style_str("rgba(10, 14, 23, 0.08)");
|
||||||
|
context_clone.fill_rect(0.0, 0.0, width, height);
|
||||||
|
|
||||||
|
// Update and draw particles
|
||||||
|
system_clone.borrow_mut().update();
|
||||||
|
system_clone.borrow().draw(&context_clone);
|
||||||
|
|
||||||
|
// Request next frame
|
||||||
|
if let Some(closure) = animate_clone.borrow().as_ref() {
|
||||||
|
window
|
||||||
|
.request_animation_frame(closure.as_ref().unchecked_ref())
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut()>));
|
||||||
|
|
||||||
|
// Start animation
|
||||||
|
if let Some(closure) = animate_closure.borrow().as_ref() {
|
||||||
|
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup resize handler
|
||||||
|
let canvas_clone = canvas.clone();
|
||||||
|
let system_resize = particle_system.clone();
|
||||||
|
let window_clone = window.clone();
|
||||||
|
|
||||||
|
let resize_closure = Closure::wrap(Box::new(move || {
|
||||||
|
let new_width = window_clone.inner_width().unwrap().as_f64().unwrap_or(800.0);
|
||||||
|
let new_height = window_clone.inner_height().unwrap().as_f64().unwrap_or(600.0);
|
||||||
|
|
||||||
|
canvas_clone.set_width(new_width as u32);
|
||||||
|
canvas_clone.set_height(new_height as u32);
|
||||||
|
|
||||||
|
system_resize.borrow_mut().resize(new_width, new_height);
|
||||||
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
|
window.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())?;
|
||||||
|
resize_closure.forget();
|
||||||
|
|
||||||
|
console::log_1(&"🌊 Particle system initialized!".into());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn build_page(document: &Document, body: &HtmlElement) -> Result<(), JsValue> {
|
fn build_page(document: &Document, body: &HtmlElement) -> Result<(), JsValue> {
|
||||||
// Hero Section
|
// Hero Section
|
||||||
let hero = create_hero(document)?;
|
let hero = create_hero(document)?;
|
||||||
|
|
@ -265,7 +496,7 @@ fn setup_scroll_animations(document: &Document) -> Result<(), JsValue> {
|
||||||
}) as Box<dyn FnMut(js_sys::Array, IntersectionObserver)>);
|
}) as Box<dyn FnMut(js_sys::Array, IntersectionObserver)>);
|
||||||
|
|
||||||
// Create observer with options
|
// Create observer with options
|
||||||
let mut options = IntersectionObserverInit::new();
|
let options = IntersectionObserverInit::new();
|
||||||
options.set_threshold(&JsValue::from_f64(0.1));
|
options.set_threshold(&JsValue::from_f64(0.1));
|
||||||
|
|
||||||
let observer = IntersectionObserver::new_with_options(
|
let observer = IntersectionObserver::new_with_options(
|
||||||
|
|
|
||||||
15
style.css
15
style.css
|
|
@ -27,6 +27,17 @@ body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Particle canvas background */
|
||||||
|
#particle-canvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading screen */
|
/* Loading screen */
|
||||||
#loading {
|
#loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -72,9 +83,9 @@ body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero::before {
|
.hero::before {
|
||||||
|
|
@ -130,6 +141,8 @@ body {
|
||||||
/* Section Styles */
|
/* Section Styles */
|
||||||
section {
|
section {
|
||||||
padding: 5rem 0;
|
padding: 5rem 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue