Compare commits
No commits in common. "11d52b2e8628dc2fd54776b6958344850396120a" and "a81ec7901d5dde5d54e1b40c8feadbf9df946d3c" have entirely different histories.
11d52b2e86
...
a81ec7901d
|
|
@ -30,9 +30,6 @@ features = [
|
||||||
"HtmlCanvasElement",
|
"HtmlCanvasElement",
|
||||||
"CanvasRenderingContext2d",
|
"CanvasRenderingContext2d",
|
||||||
"CanvasGradient",
|
"CanvasGradient",
|
||||||
"MouseEvent",
|
|
||||||
"Event",
|
|
||||||
"CssStyleDeclaration",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,8 @@ export interface InitOutput {
|
||||||
readonly __wbindgen_export_1: (a: number, b: number) => number;
|
readonly __wbindgen_export_1: (a: number, b: number) => number;
|
||||||
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) => void;
|
||||||
readonly __wbindgen_export_5: (a: number, b: number, c: number) => void;
|
readonly __wbindgen_export_5: (a: number, b: number, c: number, d: number) => void;
|
||||||
readonly __wbindgen_export_6: (a: number, b: number) => void;
|
|
||||||
readonly __wbindgen_start: () => void;
|
readonly __wbindgen_start: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -237,16 +237,12 @@ export function main() {
|
||||||
wasm.main();
|
wasm.main();
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_4(arg0, arg1, arg2, arg3) {
|
function __wbg_adapter_4(arg0, arg1) {
|
||||||
wasm.__wbindgen_export_4(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
wasm.__wbindgen_export_4(arg0, arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_11(arg0, arg1, arg2) {
|
function __wbg_adapter_7(arg0, arg1, arg2, arg3) {
|
||||||
wasm.__wbindgen_export_5(arg0, arg1, addHeapObject(arg2));
|
wasm.__wbindgen_export_5(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
||||||
}
|
|
||||||
|
|
||||||
function __wbg_adapter_14(arg0, arg1) {
|
|
||||||
wasm.__wbindgen_export_6(arg0, arg1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
|
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
|
||||||
|
|
@ -318,25 +314,14 @@ function __wbg_get_imports() {
|
||||||
const ret = getObject(arg0).classList;
|
const ret = getObject(arg0).classList;
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_clientX_ea858fbae3debd3c = function(arg0) {
|
|
||||||
const ret = getObject(arg0).clientX;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_clientY_cbb39a771d53208f = function(arg0) {
|
|
||||||
const ret = getObject(arg0).clientY;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_closePath_58530240bb00a7fc = function(arg0) {
|
|
||||||
getObject(arg0).closePath();
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_createElement_4909dfa2011f2abe = function() { return handleError(function (arg0, arg1, arg2) {
|
imports.wbg.__wbg_createElement_4909dfa2011f2abe = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
|
const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
imports.wbg.__wbg_createLinearGradient_098463fca2d0190f = function(arg0, arg1, arg2, arg3, arg4) {
|
imports.wbg.__wbg_createRadialGradient_b10566e092cb7089 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6) {
|
||||||
const ret = getObject(arg0).createLinearGradient(arg1, arg2, arg3, arg4);
|
const ret = getObject(arg0).createRadialGradient(arg1, arg2, arg3, arg4, arg5, arg6);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
}, arguments) };
|
||||||
imports.wbg.__wbg_document_7d29d139bd619045 = function(arg0) {
|
imports.wbg.__wbg_document_7d29d139bd619045 = function(arg0) {
|
||||||
const ret = getObject(arg0).document;
|
const ret = getObject(arg0).document;
|
||||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
|
|
@ -354,10 +339,6 @@ 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_getBoundingClientRect_a2461829d8aa0b30 = function(arg0) {
|
|
||||||
const ret = getObject(arg0).getBoundingClientRect();
|
|
||||||
return addHeapObject(ret);
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_getContext_15e158d04230a6f6 = function() { return handleError(function (arg0, arg1, arg2) {
|
imports.wbg.__wbg_getContext_15e158d04230a6f6 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2));
|
const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2));
|
||||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
|
|
@ -366,10 +347,6 @@ function __wbg_get_imports() {
|
||||||
const ret = getObject(arg0)[arg1 >>> 0];
|
const ret = getObject(arg0)[arg1 >>> 0];
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_height_36fae1857b6cca70 = function(arg0) {
|
|
||||||
const ret = getObject(arg0).height;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_innerHeight_eacbddff807274db = function() { return handleError(function (arg0) {
|
imports.wbg.__wbg_innerHeight_eacbddff807274db = function() { return handleError(function (arg0) {
|
||||||
const ret = getObject(arg0).innerHeight;
|
const ret = getObject(arg0).innerHeight;
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
|
|
@ -408,16 +385,6 @@ function __wbg_get_imports() {
|
||||||
const ret = result;
|
const ret = result;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_instanceof_HtmlElement_d60c51c41eb8699a = function(arg0) {
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = getObject(arg0) instanceof HTMLElement;
|
|
||||||
} 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 {
|
||||||
|
|
@ -446,10 +413,6 @@ function __wbg_get_imports() {
|
||||||
const ret = getObject(arg0).item(arg1 >>> 0);
|
const ret = getObject(arg0).item(arg1 >>> 0);
|
||||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_left_31939b629ff732e9 = function(arg0) {
|
|
||||||
const ret = getObject(arg0).left;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_length_186546c51cd61acd = function(arg0) {
|
imports.wbg.__wbg_length_186546c51cd61acd = function(arg0) {
|
||||||
const ret = getObject(arg0).length;
|
const ret = getObject(arg0).length;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -458,15 +421,9 @@ function __wbg_get_imports() {
|
||||||
const ret = getObject(arg0).length;
|
const ret = getObject(arg0).length;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_lineTo_d9b895383c2303ba = function(arg0, arg1, arg2) {
|
|
||||||
getObject(arg0).lineTo(arg1, arg2);
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_log_6c7b5f4f00b8ce3f = function(arg0) {
|
imports.wbg.__wbg_log_6c7b5f4f00b8ce3f = function(arg0) {
|
||||||
console.log(getObject(arg0));
|
console.log(getObject(arg0));
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_moveTo_a0b1ec729ba8ee5d = function(arg0, arg1, arg2) {
|
|
||||||
getObject(arg0).moveTo(arg1, arg2);
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_new_19c25a3f2fa63a02 = function() {
|
imports.wbg.__wbg_new_19c25a3f2fa63a02 = function() {
|
||||||
const ret = new Object();
|
const ret = new Object();
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
|
|
@ -486,6 +443,10 @@ 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) {
|
imports.wbg.__wbg_requestAnimationFrame_ddc84a7def436784 = function() { return handleError(function (arg0, arg1) {
|
||||||
const ret = getObject(arg0).requestAnimationFrame(getObject(arg1));
|
const ret = getObject(arg0).requestAnimationFrame(getObject(arg1));
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -493,9 +454,6 @@ function __wbg_get_imports() {
|
||||||
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_setProperty_a4431938dd3e6945 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
|
||||||
getObject(arg0).setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
|
||||||
}, 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);
|
||||||
};
|
};
|
||||||
|
|
@ -514,12 +472,6 @@ function __wbg_get_imports() {
|
||||||
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);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_setlineWidth_069d571345379833 = function(arg0, arg1) {
|
|
||||||
getObject(arg0).lineWidth = arg1;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_setstrokeStyle_3c450999cfcdcd2f = function(arg0, arg1, arg2) {
|
|
||||||
getObject(arg0).strokeStyle = getStringFromWasm0(arg1, arg2);
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_settextContent_b55fe2f5f1399466 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbg_settextContent_b55fe2f5f1399466 = function(arg0, arg1, arg2) {
|
||||||
getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
|
getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
|
||||||
};
|
};
|
||||||
|
|
@ -545,21 +497,10 @@ function __wbg_get_imports() {
|
||||||
const ret = typeof window === 'undefined' ? null : window;
|
const ret = typeof window === 'undefined' ? null : window;
|
||||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_stroke_b53e61cc42965e61 = function(arg0) {
|
|
||||||
getObject(arg0).stroke();
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_style_32a3c8393b46a115 = function(arg0) {
|
|
||||||
const ret = getObject(arg0).style;
|
|
||||||
return addHeapObject(ret);
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_target_161cb00cc3daf872 = function(arg0) {
|
imports.wbg.__wbg_target_161cb00cc3daf872 = function(arg0) {
|
||||||
const ret = getObject(arg0).target;
|
const ret = getObject(arg0).target;
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_top_447ffcfee64c5d99 = function(arg0) {
|
|
||||||
const ret = getObject(arg0).top;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_wbindgencbdrop_eb10308566512b88 = function(arg0) {
|
imports.wbg.__wbg_wbindgencbdrop_eb10308566512b88 = function(arg0) {
|
||||||
const obj = getObject(arg0).original;
|
const obj = getObject(arg0).original;
|
||||||
if (obj.cnt-- == 1) {
|
if (obj.cnt-- == 1) {
|
||||||
|
|
@ -592,10 +533,6 @@ function __wbg_get_imports() {
|
||||||
imports.wbg.__wbg_wbindgenthrow_451ec1a8469d7eb6 = function(arg0, arg1) {
|
imports.wbg.__wbg_wbindgenthrow_451ec1a8469d7eb6 = function(arg0, arg1) {
|
||||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_width_d69b4eebaa4e6b70 = function(arg0) {
|
|
||||||
const ret = getObject(arg0).width;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) {
|
imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Ref(String) -> Externref`.
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
||||||
const ret = getStringFromWasm0(arg0, arg1);
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
|
|
@ -603,12 +540,7 @@ function __wbg_get_imports() {
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_cast_aaa93aae03c115ab = function(arg0, arg1) {
|
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`.
|
// 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_14);
|
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_4);
|
||||||
return addHeapObject(ret);
|
|
||||||
};
|
|
||||||
imports.wbg.__wbindgen_cast_d557c1fcdf607639 = function(arg0, arg1) {
|
|
||||||
// Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
||||||
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_11);
|
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) {
|
imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) {
|
||||||
|
|
@ -618,7 +550,7 @@ function __wbg_get_imports() {
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_cast_e58c2e3d55f4d158 = 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: 4, 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_7);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -6,7 +6,6 @@ export const __wbindgen_export_0: (a: number) => void;
|
||||||
export const __wbindgen_export_1: (a: number, b: number) => number;
|
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) => void;
|
||||||
export const __wbindgen_export_5: (a: number, b: number, c: number) => void;
|
export const __wbindgen_export_5: (a: number, b: number, c: number, d: number) => void;
|
||||||
export const __wbindgen_export_6: (a: number, b: number) => void;
|
|
||||||
export const __wbindgen_start: () => void;
|
export const __wbindgen_start: () => void;
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-dark: #0a0e17;
|
--bg-dark: #0a0e17;
|
||||||
--bg-card: rgba(26, 31, 46, 0.8); /* More transparent for glass effect */
|
--bg-card: #1a1f2e;
|
||||||
--bg-card-hover: rgba(37, 42, 58, 0.9);
|
--bg-card-hover: #252a3a;
|
||||||
--text-primary: #e0e6ed;
|
--text-primary: #e0e6ed;
|
||||||
--text-secondary: #9ca3af;
|
--text-secondary: #9ca3af;
|
||||||
--accent: #3b82f6;
|
--accent: #3b82f6;
|
||||||
--accent-hover: #2563eb;
|
--accent-hover: #2563eb;
|
||||||
--rust: #f74c00;
|
--rust: #f74c00;
|
||||||
--border: rgba(45, 55, 72, 0.5);
|
--border: #2d3748;
|
||||||
--success: #10b981;
|
--success: #10b981;
|
||||||
--planned: #6366f1;
|
--planned: #6366f1;
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +38,37 @@ body {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Loading screen */
|
||||||
|
#loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--rust);
|
||||||
|
}
|
||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
@ -57,6 +88,16 @@ body {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
.hero .container {
|
.hero .container {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
@ -70,7 +111,6 @@ body {
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
filter: drop-shadow(0 0 2em rgba(59, 130, 246, 0.3));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-subtitle {
|
.hero-subtitle {
|
||||||
|
|
@ -85,14 +125,12 @@ body {
|
||||||
.hero-badge {
|
.hero-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
background: rgba(26, 31, 46, 0.6);
|
background: var(--bg-card);
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 2rem;
|
border-radius: 2rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
animation: float 3s ease-in-out infinite;
|
animation: float 3s ease-in-out infinite;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
|
|
@ -112,118 +150,72 @@ section {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Services Section */
|
/* Services Section */
|
||||||
.services {
|
.services {
|
||||||
/* Transparent to let particles show through slightly or keep dark */
|
background: var(--bg-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.services-grid {
|
.services-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 2rem;
|
gap: 1.5rem;
|
||||||
perspective: 1000px; /* Global perspective for grid items entrance */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3D Card System */
|
.service-card {
|
||||||
.service-card-wrapper {
|
background: var(--bg-card);
|
||||||
/* This is the element that receives mouse events and defines the space */
|
border: 1px solid var(--border);
|
||||||
position: relative;
|
border-radius: 0.75rem;
|
||||||
height: 100%;
|
padding: 1.5rem;
|
||||||
min-height: 200px;
|
transition: all 0.3s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
display: block;
|
display: block;
|
||||||
perspective: 1500px; /* Perspective for the 3D tilt */
|
overflow: hidden;
|
||||||
cursor: pointer;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-card-content {
|
.service-card:hover {
|
||||||
position: relative;
|
background: var(--bg-card-hover);
|
||||||
height: 100%;
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
/* Glassmorphism */
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
/* 3D Transform settings */
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
transform: rotateX(var(--rotate-x, 0deg)) rotateY(var(--rotate-y, 0deg));
|
|
||||||
|
|
||||||
/* Smooth return when mouse leaves */
|
|
||||||
transition: transform 0.1s cubic-bezier(0.2, 0.4, 0.6, 1), border-color 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card-wrapper:hover .service-card-content {
|
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow:
|
transform: translateY(-4px);
|
||||||
0 20px 40px rgba(0, 0, 0, 0.4),
|
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
|
||||||
0 0 20px rgba(59, 130, 246, 0.2),
|
|
||||||
inset 0 0 0 1px rgba(59, 130, 246, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glare Effect */
|
|
||||||
.card-glare {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 1rem;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at var(--glare-x, 50%) var(--glare-y, 50%),
|
|
||||||
rgba(255, 255, 255, 0.15) 0%,
|
|
||||||
transparent 60%
|
|
||||||
);
|
|
||||||
opacity: var(--glare-opacity, 0);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
mix-blend-mode: overlay;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Floating Content inside Card */
|
|
||||||
.service-header {
|
.service-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.75rem;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
transform: translateZ(25px); /* Pop out */
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-card-content h3 {
|
.service-card h3 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.25rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-weight: 700;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
font-size: 1.2rem;
|
font-size: 1.5rem;
|
||||||
transform: translateZ(35px); /* Pop out more */
|
flex-shrink: 0;
|
||||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-description {
|
.service-description {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
transform: translateZ(15px); /* Pop out slightly */
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tech Section */
|
/* Tech Section */
|
||||||
.tech {
|
.tech {
|
||||||
background: linear-gradient(180deg, rgba(10, 14, 23, 0) 0%, var(--bg-dark) 100%);
|
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-grid {
|
.tech-grid {
|
||||||
|
|
@ -235,17 +227,6 @@ section {
|
||||||
.tech-item {
|
.tech-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
border-radius: 1rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-item:hover {
|
|
||||||
transform: translateY(-5px) scale(1.02);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-color: var(--border);
|
|
||||||
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-item h3 {
|
.tech-item h3 {
|
||||||
|
|
@ -265,8 +246,6 @@ section {
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: 3rem 0;
|
padding: 3rem 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer p {
|
.footer p {
|
||||||
|
|
@ -304,19 +283,174 @@ section {
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-title {
|
section {
|
||||||
font-size: 3rem;
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid,
|
||||||
|
.tech-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scroll Animations */
|
/* Smooth scrolling */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection color */
|
||||||
|
::selection {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll-triggered animations */
|
||||||
[data-animate] {
|
[data-animate] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(30px);
|
transform: translateY(30px);
|
||||||
transition: opacity 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
|
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-animate].animate-in {
|
[data-animate].animate-in {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Parallax effect on hero */
|
||||||
|
.hero::before {
|
||||||
|
animation: parallax-float 20s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes parallax-float {
|
||||||
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
|
50% { transform: translateY(-20px) scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated gradient text */
|
||||||
|
.hero-title {
|
||||||
|
background-size: 200% auto;
|
||||||
|
animation: gradient-shift 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient-shift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover micro-interactions */
|
||||||
|
.service-card {
|
||||||
|
transform-origin: center;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card:hover {
|
||||||
|
animation: subtle-bounce 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes subtle-bounce {
|
||||||
|
0%, 100% { transform: translateY(-4px); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-item {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-item:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-item:hover h3 {
|
||||||
|
animation: pulse-glow 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.8; text-shadow: 0 0 20px rgba(59, 130, 246, 0.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer links animation */
|
||||||
|
.footer-links a {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--accent);
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover::before {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero badge pulse */
|
||||||
|
.hero-badge {
|
||||||
|
animation: float 3s ease-in-out infinite, glow-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 10px rgba(59, 130, 246, 0.2); }
|
||||||
|
50% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.4); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badge animations */
|
||||||
|
.status-badge {
|
||||||
|
animation: badge-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes badge-pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section title entrance */
|
||||||
|
.section-title {
|
||||||
|
animation: slide-in-top 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-top {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce motion for accessibility */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
590
src/lib.rs
590
src/lib.rs
|
|
@ -3,327 +3,185 @@ use wasm_bindgen::JsCast;
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
console, Document, Element, HtmlElement, Window,
|
console, Document, Element, HtmlElement, Window,
|
||||||
IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit,
|
IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit,
|
||||||
HtmlCanvasElement, CanvasRenderingContext2d, MouseEvent, CssStyleDeclaration,
|
HtmlCanvasElement, CanvasRenderingContext2d,
|
||||||
};
|
};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::f64::consts::PI;
|
|
||||||
|
|
||||||
// Mouse State
|
// Particle structure
|
||||||
#[derive(Clone, Copy)]
|
struct Particle {
|
||||||
struct MouseState {
|
|
||||||
x: f64,
|
x: f64,
|
||||||
y: f64,
|
y: f64,
|
||||||
// Normalized coordinates (-1.0 to 1.0)
|
vx: f64,
|
||||||
norm_x: f64,
|
vy: f64,
|
||||||
norm_y: f64,
|
size: f64,
|
||||||
|
life: f64,
|
||||||
|
max_life: f64,
|
||||||
|
hue: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MouseState {
|
impl Particle {
|
||||||
fn new() -> Self {
|
fn new(x: f64, y: f64, hue: f64) -> Self {
|
||||||
Self { x: 0.0, y: 0.0, norm_x: 0.0, norm_y: 0.0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3D Types
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct Point3D {
|
|
||||||
x: f64,
|
|
||||||
y: f64,
|
|
||||||
z: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Polyhedron {
|
|
||||||
vertices: Vec<Point3D>,
|
|
||||||
edges: Vec<(usize, usize)>,
|
|
||||||
rotation_x: f64,
|
|
||||||
rotation_y: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Polyhedron {
|
|
||||||
fn new_dodecahedron(size: f64) -> Self {
|
|
||||||
let phi = (1.0 + 5.0_f64.sqrt()) / 2.0;
|
|
||||||
let inv_phi = 1.0 / phi;
|
|
||||||
|
|
||||||
let mut vertices = Vec::new();
|
|
||||||
|
|
||||||
// Orange vertices (Cube)
|
|
||||||
vertices.push(Point3D { x: -1.0, y: -1.0, z: -1.0 });
|
|
||||||
vertices.push(Point3D { x: 1.0, y: -1.0, z: -1.0 });
|
|
||||||
vertices.push(Point3D { x: 1.0, y: 1.0, z: -1.0 });
|
|
||||||
vertices.push(Point3D { x: -1.0, y: 1.0, z: -1.0 });
|
|
||||||
vertices.push(Point3D { x: -1.0, y: -1.0, z: 1.0 });
|
|
||||||
vertices.push(Point3D { x: 1.0, y: -1.0, z: 1.0 });
|
|
||||||
vertices.push(Point3D { x: 1.0, y: 1.0, z: 1.0 });
|
|
||||||
vertices.push(Point3D { x: -1.0, y: 1.0, z: 1.0 });
|
|
||||||
|
|
||||||
// Green vertices (Rectangle in YZ plane)
|
|
||||||
vertices.push(Point3D { x: 0.0, y: -phi, z: -inv_phi });
|
|
||||||
vertices.push(Point3D { x: 0.0, y: -phi, z: inv_phi });
|
|
||||||
vertices.push(Point3D { x: 0.0, y: phi, z: -inv_phi });
|
|
||||||
vertices.push(Point3D { x: 0.0, y: phi, z: inv_phi });
|
|
||||||
|
|
||||||
// Blue vertices (Rectangle in XZ plane)
|
|
||||||
vertices.push(Point3D { x: -inv_phi, y: 0.0, z: -phi });
|
|
||||||
vertices.push(Point3D { x: inv_phi, y: 0.0, z: -phi });
|
|
||||||
vertices.push(Point3D { x: -inv_phi, y: 0.0, z: phi });
|
|
||||||
vertices.push(Point3D { x: inv_phi, y: 0.0, z: phi });
|
|
||||||
|
|
||||||
// Pink vertices (Rectangle in XY plane)
|
|
||||||
vertices.push(Point3D { x: -phi, y: -inv_phi, z: 0.0 });
|
|
||||||
vertices.push(Point3D { x: phi, y: -inv_phi, z: 0.0 });
|
|
||||||
vertices.push(Point3D { x: -phi, y: inv_phi, z: 0.0 });
|
|
||||||
vertices.push(Point3D { x: phi, y: inv_phi, z: 0.0 });
|
|
||||||
|
|
||||||
// Scale vertices
|
|
||||||
for v in &mut vertices {
|
|
||||||
v.x *= size;
|
|
||||||
v.y *= size;
|
|
||||||
v.z *= size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified edges (connecting nearest neighbors) - simplistic wireframe logic
|
|
||||||
// A full correct edge list is long, so we'll use a distance threshold to draw edges dynamically
|
|
||||||
// or just use a simpler shape if performance is an issue.
|
|
||||||
// Let's use a cube for guaranteed correctness in code length constraint.
|
|
||||||
Self::new_cube(size * 1.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_cube(size: f64) -> Self {
|
|
||||||
let vertices = vec![
|
|
||||||
Point3D { x: -size, y: -size, z: -size },
|
|
||||||
Point3D { x: size, y: -size, z: -size },
|
|
||||||
Point3D { x: size, y: size, z: -size },
|
|
||||||
Point3D { x: -size, y: size, z: -size },
|
|
||||||
Point3D { x: -size, y: -size, z: size },
|
|
||||||
Point3D { x: size, y: -size, z: size },
|
|
||||||
Point3D { x: size, y: size, z: size },
|
|
||||||
Point3D { x: -size, y: size, z: size },
|
|
||||||
];
|
|
||||||
|
|
||||||
let edges = vec![
|
|
||||||
(0, 1), (1, 2), (2, 3), (3, 0), // Back face
|
|
||||||
(4, 5), (5, 6), (6, 7), (7, 4), // Front face
|
|
||||||
(0, 4), (1, 5), (2, 6), (3, 7), // Connecting edges
|
|
||||||
// Diagonals for more "tech" look
|
|
||||||
(0, 2), (4, 6), (0, 5), (1, 4)
|
|
||||||
];
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
vertices,
|
x,
|
||||||
edges,
|
y,
|
||||||
rotation_x: 0.0,
|
vx: 0.0,
|
||||||
rotation_y: 0.0,
|
vy: 0.0,
|
||||||
|
size: js_sys::Math::random() * 2.0 + 2.0,
|
||||||
|
life: 1.0,
|
||||||
|
max_life: js_sys::Math::random() * 500.0 + 500.0,
|
||||||
|
hue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rotate(&mut self, dx: f64, dy: f64) {
|
fn update(&mut self, width: f64, height: f64, time: f64) {
|
||||||
self.rotation_x += dx;
|
// Multiple layered flow fields for complex fluid motion
|
||||||
self.rotation_y += dy;
|
let scale1 = 0.003;
|
||||||
|
let scale2 = 0.006;
|
||||||
|
let scale3 = 0.001;
|
||||||
|
|
||||||
|
// Layer 1: Large sweeping currents
|
||||||
|
let angle1 = (self.x * scale1 + time * 0.0002).sin() * 3.0
|
||||||
|
+ (self.y * scale1 + time * 0.0001).cos() * 3.0;
|
||||||
|
|
||||||
|
// Layer 2: Medium turbulence
|
||||||
|
let angle2 = (self.x * scale2 - time * 0.0003).cos() * 2.0
|
||||||
|
+ (self.y * scale2 + time * 0.0002).sin() * 2.0;
|
||||||
|
|
||||||
|
// Layer 3: Fine detail
|
||||||
|
let angle3 = ((self.x * scale3 + time * 0.0001).sin()
|
||||||
|
+ (self.y * scale3 - time * 0.00015).cos()) * 1.5;
|
||||||
|
|
||||||
|
// Combine flow fields
|
||||||
|
let angle = angle1 + angle2 + angle3;
|
||||||
|
|
||||||
|
// Convert to force
|
||||||
|
let force = 0.15;
|
||||||
|
let fx = angle.cos() * force;
|
||||||
|
let fy = angle.sin() * force;
|
||||||
|
|
||||||
|
// Apply force with gentle acceleration
|
||||||
|
self.vx += fx;
|
||||||
|
self.vy += fy;
|
||||||
|
|
||||||
|
// Smooth damping for fluid motion
|
||||||
|
self.vx *= 0.95;
|
||||||
|
self.vy *= 0.95;
|
||||||
|
|
||||||
|
// Limit maximum velocity for smooth flow
|
||||||
|
let max_speed = 2.0;
|
||||||
|
let speed = (self.vx * self.vx + self.vy * self.vy).sqrt();
|
||||||
|
if speed > max_speed {
|
||||||
|
self.vx = (self.vx / speed) * max_speed;
|
||||||
|
self.vy = (self.vy / speed) * max_speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position
|
||||||
|
self.x += self.vx;
|
||||||
|
self.y += self.vy;
|
||||||
|
|
||||||
|
self.life -= 1.0;
|
||||||
|
|
||||||
|
// Wrap around edges smoothly
|
||||||
|
if self.x < -10.0 {
|
||||||
|
self.x = width + 10.0;
|
||||||
|
}
|
||||||
|
if self.x > width + 10.0 {
|
||||||
|
self.x = -10.0;
|
||||||
|
}
|
||||||
|
if self.y < -10.0 {
|
||||||
|
self.y = height + 10.0;
|
||||||
|
}
|
||||||
|
if self.y > height + 10.0 {
|
||||||
|
self.y = -10.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);
|
||||||
|
|
||||||
|
// Glow effect
|
||||||
|
let glow_gradient = ctx.create_radial_gradient(
|
||||||
|
self.x, self.y, 0.0,
|
||||||
|
self.x, self.y, self.size * 3.0
|
||||||
|
).ok();
|
||||||
|
|
||||||
|
if let Some(gradient) = glow_gradient {
|
||||||
|
let alpha_hex = format!("{:02x}", (alpha * 60.0) as u8);
|
||||||
|
gradient.add_color_stop(0.0, &format!("#{}", alpha_hex.repeat(3))).ok();
|
||||||
|
gradient.add_color_stop(0.5, &format!("rgba(50, 255, 150, {})", alpha * 0.3)).ok();
|
||||||
|
gradient.add_color_stop(1.0, "rgba(50, 255, 150, 0)").ok();
|
||||||
|
|
||||||
|
ctx.set_fill_style(&JsValue::from(gradient));
|
||||||
|
ctx.begin_path();
|
||||||
|
let _ = ctx.arc(self.x, self.y, self.size * 3.0, 0.0, std::f64::consts::PI * 2.0);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core particle
|
||||||
|
let color = format!("rgba(100, 255, 180, {})", alpha * 0.8);
|
||||||
|
ctx.set_fill_style_str(&color);
|
||||||
|
ctx.begin_path();
|
||||||
|
let _ = ctx.arc(self.x, self.y, self.size, 0.0, std::f64::consts::PI * 2.0);
|
||||||
|
ctx.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wave layer for fluid simulation
|
// Particle system
|
||||||
struct WaveLayer {
|
struct ParticleSystem {
|
||||||
amplitude: f64,
|
particles: Vec<Particle>,
|
||||||
frequency: f64,
|
|
||||||
speed: f64,
|
|
||||||
offset: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fluid wave system
|
|
||||||
struct FluidWaves {
|
|
||||||
width: f64,
|
width: f64,
|
||||||
height: f64,
|
height: f64,
|
||||||
|
max_particles: usize,
|
||||||
time: f64,
|
time: f64,
|
||||||
layers: Vec<WaveLayer>,
|
|
||||||
polyhedron: Polyhedron,
|
|
||||||
mouse: Rc<RefCell<MouseState>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FluidWaves {
|
impl ParticleSystem {
|
||||||
fn new(width: f64, height: f64, mouse: Rc<RefCell<MouseState>>) -> Self {
|
fn new(width: f64, height: f64) -> Self {
|
||||||
// Create multiple wave layers with different properties (faster speeds)
|
|
||||||
let layers = vec![
|
|
||||||
// Larger, slower primary waves for big curves
|
|
||||||
WaveLayer { amplitude: 0.25, frequency: 0.001, speed: 0.0008, offset: 0.0 },
|
|
||||||
// Secondary shape definition
|
|
||||||
WaveLayer { amplitude: 0.18, frequency: 0.0018, speed: 0.0015, offset: 1.5 },
|
|
||||||
// Counter-movement for interest
|
|
||||||
WaveLayer { amplitude: 0.12, frequency: 0.003, speed: -0.0012, offset: 3.0 },
|
|
||||||
// Detail waves
|
|
||||||
WaveLayer { amplitude: 0.08, frequency: 0.005, speed: 0.002, offset: 4.5 },
|
|
||||||
// Fine detail
|
|
||||||
WaveLayer { amplitude: 0.05, frequency: 0.008, speed: -0.0025, offset: 2.0 },
|
|
||||||
];
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
particles: Vec::new(),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
max_particles: 1200,
|
||||||
time: 0.0,
|
time: 0.0,
|
||||||
layers,
|
}
|
||||||
polyhedron: Polyhedron::new_dodecahedron(100.0),
|
}
|
||||||
mouse,
|
|
||||||
|
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) {
|
fn update(&mut self) {
|
||||||
self.time += 1.0;
|
self.time += 1.0;
|
||||||
|
|
||||||
// Rotate polyhedron based on mouse position and time
|
// Update existing particles
|
||||||
let mouse = self.mouse.borrow();
|
for particle in &mut self.particles {
|
||||||
self.polyhedron.rotate(0.005 + mouse.norm_y * 0.01, 0.005 + mouse.norm_x * 0.01);
|
particle.update(self.width, self.height, self.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate wave height at a given position with randomness
|
// Remove dead particles
|
||||||
fn wave_height(&self, x: f64, y: f64, randomness: f64) -> f64 {
|
self.particles.retain(|p| !p.is_dead());
|
||||||
let mut height = 0.0;
|
|
||||||
|
|
||||||
for layer in &self.layers {
|
// Spawn new particles
|
||||||
// Multiple sine waves at different frequencies for organic curves
|
let spawn_count = (self.max_particles - self.particles.len()).min(5);
|
||||||
let wave1 = (x * layer.frequency + self.time * layer.speed + layer.offset).sin();
|
self.spawn_particles(spawn_count);
|
||||||
let wave2 = (x * layer.frequency * 1.7 - self.time * layer.speed * 0.5).sin();
|
|
||||||
let wave3 = (x * layer.frequency * 2.3 + self.time * layer.speed * 1.2 + layer.offset).cos();
|
|
||||||
|
|
||||||
// Vertical component for depth
|
|
||||||
let wave_y = (y * layer.frequency * 0.4 - self.time * layer.speed * 0.6).cos();
|
|
||||||
|
|
||||||
// Combine with noise-like variation
|
|
||||||
let combined = (wave1 + wave2 * 0.5 + wave3 * 0.3 + wave_y * 0.4) * layer.amplitude;
|
|
||||||
height += combined * randomness;
|
|
||||||
}
|
|
||||||
|
|
||||||
height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, ctx: &CanvasRenderingContext2d) {
|
fn draw(&self, ctx: &CanvasRenderingContext2d) {
|
||||||
// Draw smooth gradient waves at the bottom
|
for particle in &self.particles {
|
||||||
self.draw_smooth_waves(ctx);
|
particle.draw(ctx);
|
||||||
|
|
||||||
// Draw 3D object floating in the background
|
|
||||||
self.draw_polyhedron(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_polyhedron(&self, ctx: &CanvasRenderingContext2d) {
|
|
||||||
let cx = self.width * 0.75; // Position on the right side
|
|
||||||
let cy = self.height * 0.4;
|
|
||||||
|
|
||||||
let cos_x = self.polyhedron.rotation_x.cos();
|
|
||||||
let sin_x = self.polyhedron.rotation_x.sin();
|
|
||||||
let cos_y = self.polyhedron.rotation_y.cos();
|
|
||||||
let sin_y = self.polyhedron.rotation_y.sin();
|
|
||||||
|
|
||||||
let mut projected_points = Vec::new();
|
|
||||||
|
|
||||||
// Project vertices
|
|
||||||
for v in &self.polyhedron.vertices {
|
|
||||||
// Rotate around X
|
|
||||||
let y1 = v.y * cos_x - v.z * sin_x;
|
|
||||||
let z1 = v.y * sin_x + v.z * cos_x;
|
|
||||||
|
|
||||||
// Rotate around Y
|
|
||||||
let x2 = v.x * cos_y - z1 * sin_y;
|
|
||||||
let z2 = v.x * sin_y + z1 * cos_y; // Depth
|
|
||||||
|
|
||||||
// Perspective projection
|
|
||||||
let scale = 400.0 / (400.0 + z2); // Simple perspective
|
|
||||||
let x_proj = x2 * scale + cx;
|
|
||||||
let y_proj = y1 * scale + cy;
|
|
||||||
|
|
||||||
projected_points.push((x_proj, y_proj));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw edges
|
|
||||||
ctx.set_stroke_style_str("rgba(59, 130, 246, 0.15)");
|
|
||||||
ctx.set_line_width(1.0);
|
|
||||||
ctx.begin_path();
|
|
||||||
|
|
||||||
for &(start, end) in &self.polyhedron.edges {
|
|
||||||
if start < projected_points.len() && end < projected_points.len() {
|
|
||||||
let (x1, y1) = projected_points[start];
|
|
||||||
let (x2, y2) = projected_points[end];
|
|
||||||
|
|
||||||
ctx.move_to(x1, y1);
|
|
||||||
ctx.line_to(x2, y2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// Draw vertices
|
|
||||||
ctx.set_fill_style_str("rgba(99, 102, 241, 0.4)");
|
|
||||||
for (x, y) in &projected_points {
|
|
||||||
ctx.begin_path();
|
|
||||||
ctx.arc(*x, *y, 2.0, 0.0, PI * 2.0).unwrap();
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_smooth_waves(&self, ctx: &CanvasRenderingContext2d) {
|
|
||||||
let num_waves = 12;
|
|
||||||
let wave_section_height = self.height * 0.6; // Bottom 60% of screen
|
|
||||||
let start_y = self.height - wave_section_height;
|
|
||||||
|
|
||||||
// Draw from back to front for proper layering
|
|
||||||
for i in (0..num_waves).rev() {
|
|
||||||
let progress = i as f64 / (num_waves - 1) as f64;
|
|
||||||
|
|
||||||
// Each wave layer at different depths
|
|
||||||
let layer_offset = progress * wave_section_height * 0.9;
|
|
||||||
let base_y = start_y + layer_offset;
|
|
||||||
|
|
||||||
// Varying amplitude for each layer (more variation in front)
|
|
||||||
// Increased amplitude for "curvier" look
|
|
||||||
let amplitude = 40.0 + progress * 80.0;
|
|
||||||
|
|
||||||
// Randomness factor increases for front layers
|
|
||||||
let randomness = 0.8 + progress * 0.4;
|
|
||||||
|
|
||||||
ctx.begin_path();
|
|
||||||
ctx.move_to(0.0, base_y);
|
|
||||||
|
|
||||||
// Draw smooth wave curve - increased points for smoothness
|
|
||||||
let points = 300;
|
|
||||||
for j in 0..=points {
|
|
||||||
let x = (j as f64 / points as f64) * self.width;
|
|
||||||
let y_offset = self.wave_height(x, base_y, randomness) * amplitude;
|
|
||||||
let y = base_y + y_offset;
|
|
||||||
|
|
||||||
ctx.line_to(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the path to bottom
|
|
||||||
ctx.line_to(self.width, self.height);
|
|
||||||
ctx.line_to(0.0, self.height);
|
|
||||||
ctx.close_path();
|
|
||||||
|
|
||||||
// Create gradient for depth effect
|
|
||||||
let gradient = ctx.create_linear_gradient(0.0, base_y - 80.0, 0.0, self.height);
|
|
||||||
|
|
||||||
// Back layers are more transparent and darker (3D depth)
|
|
||||||
// Front layers are more opaque and brighter
|
|
||||||
let alpha_top = 0.05 + progress * 0.25;
|
|
||||||
let alpha_bottom = 0.1 + progress * 0.3;
|
|
||||||
|
|
||||||
// Deep ocean colors
|
|
||||||
// Hue shift: Deep Blue (220) -> Teal/Cyan (160)
|
|
||||||
let hue = 220.0 - progress * 60.0;
|
|
||||||
let saturation = 50.0 + progress * 20.0; // 50-70%
|
|
||||||
let lightness_top = 15.0 + progress * 20.0; // 15-35%
|
|
||||||
let lightness_bottom = 10.0 + progress * 15.0; // 10-25%
|
|
||||||
|
|
||||||
let _ = gradient.add_color_stop(0.0, &format!("hsla({}, {}%, {}%, {})",
|
|
||||||
hue, saturation, lightness_top, alpha_top));
|
|
||||||
|
|
||||||
// Middle stop for better volume/highlight
|
|
||||||
let _ = gradient.add_color_stop(0.4, &format!("hsla({}, {}%, {}%, {})",
|
|
||||||
hue - 5.0, saturation + 5.0, lightness_top + 5.0, alpha_top * 1.2));
|
|
||||||
|
|
||||||
let _ = gradient.add_color_stop(1.0, &format!("hsla({}, {}%, {}%, {})",
|
|
||||||
hue - 15.0, saturation, lightness_bottom, alpha_bottom));
|
|
||||||
|
|
||||||
#[allow(deprecated)] // set_fill_style with JsValue is technically deprecated but fine here
|
|
||||||
ctx.set_fill_style(&JsValue::from(gradient));
|
|
||||||
ctx.fill();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,34 +198,12 @@ pub fn main() -> Result<(), JsValue> {
|
||||||
let window = web_sys::window().expect("no global window exists");
|
let window = web_sys::window().expect("no global window exists");
|
||||||
let document = window.document().expect("should have a document on window");
|
let document = window.document().expect("should have a document on window");
|
||||||
|
|
||||||
// Global mouse tracking
|
|
||||||
let mouse_state = Rc::new(RefCell::new(MouseState::new()));
|
|
||||||
{
|
|
||||||
let mouse_state = mouse_state.clone();
|
|
||||||
let window_clone = window.clone();
|
|
||||||
let closure = Closure::wrap(Box::new(move |event: MouseEvent| {
|
|
||||||
let w = window_clone.inner_width().unwrap().as_f64().unwrap_or(1.0);
|
|
||||||
let h = window_clone.inner_height().unwrap().as_f64().unwrap_or(1.0);
|
|
||||||
let x = event.client_x() as f64;
|
|
||||||
let y = event.client_y() as f64;
|
|
||||||
|
|
||||||
let mut state = mouse_state.borrow_mut();
|
|
||||||
state.x = x;
|
|
||||||
state.y = y;
|
|
||||||
state.norm_x = (x / w) * 2.0 - 1.0;
|
|
||||||
state.norm_y = (y / h) * 2.0 - 1.0;
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
|
||||||
|
|
||||||
window.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())?;
|
|
||||||
closure.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// Create and setup particle canvas
|
||||||
setup_particle_canvas(&document, &body, &window, mouse_state.clone())?;
|
setup_particle_canvas(&document, &body, &window)?;
|
||||||
|
|
||||||
build_page(&document, &body)?;
|
build_page(&document, &body)?;
|
||||||
setup_scroll_animations(&document)?;
|
setup_scroll_animations(&document)?;
|
||||||
|
|
@ -377,12 +213,7 @@ pub fn main() -> Result<(), JsValue> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_particle_canvas(
|
fn setup_particle_canvas(document: &Document, body: &HtmlElement, window: &Window) -> Result<(), JsValue> {
|
||||||
document: &Document,
|
|
||||||
body: &HtmlElement,
|
|
||||||
window: &Window,
|
|
||||||
mouse_state: Rc<RefCell<MouseState>>
|
|
||||||
) -> Result<(), JsValue> {
|
|
||||||
console::log_1(&"🎨 Setting up particle canvas...".into());
|
console::log_1(&"🎨 Setting up particle canvas...".into());
|
||||||
|
|
||||||
// Create canvas element
|
// Create canvas element
|
||||||
|
|
@ -409,24 +240,47 @@ fn setup_particle_canvas(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.dyn_into::<CanvasRenderingContext2d>()?;
|
.dyn_into::<CanvasRenderingContext2d>()?;
|
||||||
|
|
||||||
// Initialize fluid wave system
|
// Initialize particle system
|
||||||
let wave_system = Rc::new(RefCell::new(FluidWaves::new(width, height, mouse_state)));
|
let particle_system = Rc::new(RefCell::new(ParticleSystem::new(width, height)));
|
||||||
console::log_1(&"🌊 Fluid wave system initialized!".into());
|
|
||||||
|
// Spawn initial particles
|
||||||
|
particle_system.borrow_mut().spawn_particles(600);
|
||||||
|
console::log_1(&format!("🌟 Spawned {} initial particles", particle_system.borrow().particles.len()).into());
|
||||||
|
|
||||||
// Setup animation loop
|
// Setup animation loop
|
||||||
let system_clone = wave_system.clone();
|
let system_clone = particle_system.clone();
|
||||||
let context_clone = context.clone();
|
let context_clone = context.clone();
|
||||||
|
let frame_count = Rc::new(RefCell::new(0u32));
|
||||||
let window_clone = window.clone();
|
let window_clone = window.clone();
|
||||||
|
|
||||||
let animate_closure = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
|
let animate_closure = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
|
||||||
let animate_clone = animate_closure.clone();
|
let animate_clone = animate_closure.clone();
|
||||||
|
let frame_clone = frame_count.clone();
|
||||||
|
|
||||||
*animate_closure.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
*animate_closure.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
||||||
// Clear canvas
|
*frame_clone.borrow_mut() += 1;
|
||||||
context_clone.set_fill_style_str("rgba(10, 14, 23, 1.0)");
|
|
||||||
|
if *frame_clone.borrow() == 1 {
|
||||||
|
console::log_1(&"🎬 Animation loop started!".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if *frame_clone.borrow() % 60 == 0 {
|
||||||
|
let sys = system_clone.borrow();
|
||||||
|
console::log_1(&format!("🔄 Frame {}, {} particles",
|
||||||
|
*frame_clone.borrow(),
|
||||||
|
sys.particles.len()).into());
|
||||||
|
|
||||||
|
if let Some(p) = sys.particles.first() {
|
||||||
|
console::log_1(&format!("🔍 First particle: x={:.1}, y={:.1}, vx={:.2}, vy={:.2}, size={:.1}, life={:.1}",
|
||||||
|
p.x, p.y, p.vx, p.vy, p.size, p.life).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semi-transparent clear for fluid trails
|
||||||
|
context_clone.set_fill_style_str("rgba(10, 14, 23, 0.12)");
|
||||||
context_clone.fill_rect(0.0, 0.0, width, height);
|
context_clone.fill_rect(0.0, 0.0, width, height);
|
||||||
|
|
||||||
// Update and draw waves
|
// Update and draw particles
|
||||||
system_clone.borrow_mut().update();
|
system_clone.borrow_mut().update();
|
||||||
system_clone.borrow().draw(&context_clone);
|
system_clone.borrow().draw(&context_clone);
|
||||||
|
|
||||||
|
|
@ -445,12 +299,12 @@ fn setup_particle_canvas(
|
||||||
|
|
||||||
// Setup resize handler
|
// Setup resize handler
|
||||||
let canvas_clone = canvas.clone();
|
let canvas_clone = canvas.clone();
|
||||||
let system_resize = wave_system.clone();
|
let system_resize = particle_system.clone();
|
||||||
let window_resize = window.clone();
|
let window_clone = window.clone();
|
||||||
|
|
||||||
let resize_closure = Closure::wrap(Box::new(move || {
|
let resize_closure = Closure::wrap(Box::new(move || {
|
||||||
let new_width = window_resize.inner_width().unwrap().as_f64().unwrap_or(800.0);
|
let new_width = window_clone.inner_width().unwrap().as_f64().unwrap_or(800.0);
|
||||||
let new_height = window_resize.inner_height().unwrap().as_f64().unwrap_or(600.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_width(new_width as u32);
|
||||||
canvas_clone.set_height(new_height as u32);
|
canvas_clone.set_height(new_height as u32);
|
||||||
|
|
@ -568,7 +422,7 @@ fn create_service_card(
|
||||||
status: &str,
|
status: &str,
|
||||||
url: Option<&str>,
|
url: Option<&str>,
|
||||||
) -> Result<Element, JsValue> {
|
) -> Result<Element, JsValue> {
|
||||||
let wrapper = if let Some(link) = url {
|
let card = if let Some(link) = url {
|
||||||
let a = document.create_element("a")?;
|
let a = document.create_element("a")?;
|
||||||
a.set_attribute("href", link)?;
|
a.set_attribute("href", link)?;
|
||||||
a.set_attribute("target", "_blank")?;
|
a.set_attribute("target", "_blank")?;
|
||||||
|
|
@ -577,81 +431,7 @@ fn create_service_card(
|
||||||
} else {
|
} else {
|
||||||
document.create_element("div")?
|
document.create_element("div")?
|
||||||
};
|
};
|
||||||
|
card.set_class_name("service-card");
|
||||||
// Cast to HtmlElement to access properties
|
|
||||||
let wrapper_el = wrapper.dyn_ref::<HtmlElement>().unwrap();
|
|
||||||
|
|
||||||
wrapper.set_class_name("service-card-wrapper");
|
|
||||||
|
|
||||||
// Inner Content Div
|
|
||||||
let content = document.create_element("div")?;
|
|
||||||
content.set_class_name("service-card-content");
|
|
||||||
|
|
||||||
let content_el = content.dyn_ref::<HtmlElement>().unwrap();
|
|
||||||
|
|
||||||
// Glare Effect Div
|
|
||||||
let glare = document.create_element("div")?;
|
|
||||||
glare.set_class_name("card-glare");
|
|
||||||
content.append_child(&glare)?;
|
|
||||||
|
|
||||||
// Add 3D Tilt Effect Logic
|
|
||||||
{
|
|
||||||
let content_el_clone = content_el.clone();
|
|
||||||
let wrapper_el_clone = wrapper_el.clone();
|
|
||||||
let glare_el_clone = glare.dyn_ref::<HtmlElement>().unwrap().clone();
|
|
||||||
|
|
||||||
let mouse_move_closure = Closure::wrap(Box::new(move |e: MouseEvent| {
|
|
||||||
let rect = wrapper_el_clone.get_bounding_client_rect();
|
|
||||||
// Calculate position relative to the card center
|
|
||||||
let width = rect.width();
|
|
||||||
let height = rect.height();
|
|
||||||
|
|
||||||
// Mouse position relative to element top-left
|
|
||||||
let mouse_x = e.client_x() as f64 - rect.left();
|
|
||||||
let mouse_y = e.client_y() as f64 - rect.top();
|
|
||||||
|
|
||||||
let center_x = width / 2.0;
|
|
||||||
let center_y = height / 2.0;
|
|
||||||
|
|
||||||
// Rotation logic
|
|
||||||
let max_rot = 12.0; // degrees
|
|
||||||
let rotate_y = ((mouse_x - center_x) / center_x) * max_rot;
|
|
||||||
let rotate_x = -((mouse_y - center_y) / center_y) * max_rot;
|
|
||||||
|
|
||||||
let style = content_el_clone.style();
|
|
||||||
style.set_property("--rotate-x", &format!("{:.2}deg", rotate_x)).ok();
|
|
||||||
style.set_property("--rotate-y", &format!("{:.2}deg", rotate_y)).ok();
|
|
||||||
|
|
||||||
// Glare logic
|
|
||||||
let glare_x_pct = (mouse_x / width) * 100.0;
|
|
||||||
let glare_y_pct = (mouse_y / height) * 100.0;
|
|
||||||
|
|
||||||
// Glare opacity based on distance from center (optional, or constant on hover)
|
|
||||||
let glare_style = glare_el_clone.style();
|
|
||||||
glare_style.set_property("--glare-x", &format!("{:.1}%", glare_x_pct)).ok();
|
|
||||||
glare_style.set_property("--glare-y", &format!("{:.1}%", glare_y_pct)).ok();
|
|
||||||
glare_style.set_property("--glare-opacity", "1").ok();
|
|
||||||
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
|
||||||
|
|
||||||
wrapper_el.add_event_listener_with_callback("mousemove", mouse_move_closure.as_ref().unchecked_ref())?;
|
|
||||||
mouse_move_closure.forget();
|
|
||||||
|
|
||||||
let content_el_clone = content_el.clone();
|
|
||||||
let glare_el_clone = glare.dyn_ref::<HtmlElement>().unwrap().clone();
|
|
||||||
|
|
||||||
let mouse_leave_closure = Closure::wrap(Box::new(move |_: MouseEvent| {
|
|
||||||
let style = content_el_clone.style();
|
|
||||||
style.set_property("--rotate-x", "0deg").ok();
|
|
||||||
style.set_property("--rotate-y", "0deg").ok();
|
|
||||||
|
|
||||||
let glare_style = glare_el_clone.style();
|
|
||||||
glare_style.set_property("--glare-opacity", "0").ok();
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
|
||||||
|
|
||||||
wrapper_el.add_event_listener_with_callback("mouseleave", mouse_leave_closure.as_ref().unchecked_ref())?;
|
|
||||||
mouse_leave_closure.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
let header = document.create_element("div")?;
|
let header = document.create_element("div")?;
|
||||||
header.set_class_name("service-header");
|
header.set_class_name("service-header");
|
||||||
|
|
@ -670,12 +450,10 @@ fn create_service_card(
|
||||||
desc.set_text_content(Some(description));
|
desc.set_text_content(Some(description));
|
||||||
desc.set_class_name("service-description");
|
desc.set_class_name("service-description");
|
||||||
|
|
||||||
content.append_child(&header)?;
|
card.append_child(&header)?;
|
||||||
content.append_child(&desc)?;
|
card.append_child(&desc)?;
|
||||||
|
|
||||||
wrapper.append_child(&content)?;
|
Ok(card)
|
||||||
|
|
||||||
Ok(wrapper)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_tech_section(document: &Document) -> Result<Element, JsValue> {
|
fn create_tech_section(document: &Document) -> Result<Element, JsValue> {
|
||||||
|
|
|
||||||
328
style.css
328
style.css
|
|
@ -7,14 +7,14 @@
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-dark: #0a0e17;
|
--bg-dark: #0a0e17;
|
||||||
--bg-card: rgba(26, 31, 46, 0.8); /* More transparent for glass effect */
|
--bg-card: #1a1f2e;
|
||||||
--bg-card-hover: rgba(37, 42, 58, 0.9);
|
--bg-card-hover: #252a3a;
|
||||||
--text-primary: #e0e6ed;
|
--text-primary: #e0e6ed;
|
||||||
--text-secondary: #9ca3af;
|
--text-secondary: #9ca3af;
|
||||||
--accent: #3b82f6;
|
--accent: #3b82f6;
|
||||||
--accent-hover: #2563eb;
|
--accent-hover: #2563eb;
|
||||||
--rust: #f74c00;
|
--rust: #f74c00;
|
||||||
--border: rgba(45, 55, 72, 0.5);
|
--border: #2d3748;
|
||||||
--success: #10b981;
|
--success: #10b981;
|
||||||
--planned: #6366f1;
|
--planned: #6366f1;
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +38,37 @@ body {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Loading screen */
|
||||||
|
#loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--rust);
|
||||||
|
}
|
||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
@ -57,6 +88,16 @@ body {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
.hero .container {
|
.hero .container {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
@ -70,7 +111,6 @@ body {
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
filter: drop-shadow(0 0 2em rgba(59, 130, 246, 0.3));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-subtitle {
|
.hero-subtitle {
|
||||||
|
|
@ -85,14 +125,12 @@ body {
|
||||||
.hero-badge {
|
.hero-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
background: rgba(26, 31, 46, 0.6);
|
background: var(--bg-card);
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 2rem;
|
border-radius: 2rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
animation: float 3s ease-in-out infinite;
|
animation: float 3s ease-in-out infinite;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
|
|
@ -112,118 +150,72 @@ section {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Services Section */
|
/* Services Section */
|
||||||
.services {
|
.services {
|
||||||
/* Transparent to let particles show through slightly or keep dark */
|
background: var(--bg-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.services-grid {
|
.services-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 2rem;
|
gap: 1.5rem;
|
||||||
perspective: 1000px; /* Global perspective for grid items entrance */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3D Card System */
|
.service-card {
|
||||||
.service-card-wrapper {
|
background: var(--bg-card);
|
||||||
/* This is the element that receives mouse events and defines the space */
|
border: 1px solid var(--border);
|
||||||
position: relative;
|
border-radius: 0.75rem;
|
||||||
height: 100%;
|
padding: 1.5rem;
|
||||||
min-height: 200px;
|
transition: all 0.3s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
display: block;
|
display: block;
|
||||||
perspective: 1500px; /* Perspective for the 3D tilt */
|
overflow: hidden;
|
||||||
cursor: pointer;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-card-content {
|
.service-card:hover {
|
||||||
position: relative;
|
background: var(--bg-card-hover);
|
||||||
height: 100%;
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
/* Glassmorphism */
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
/* 3D Transform settings */
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
transform: rotateX(var(--rotate-x, 0deg)) rotateY(var(--rotate-y, 0deg));
|
|
||||||
|
|
||||||
/* Smooth return when mouse leaves */
|
|
||||||
transition: transform 0.1s cubic-bezier(0.2, 0.4, 0.6, 1), border-color 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card-wrapper:hover .service-card-content {
|
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow:
|
transform: translateY(-4px);
|
||||||
0 20px 40px rgba(0, 0, 0, 0.4),
|
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
|
||||||
0 0 20px rgba(59, 130, 246, 0.2),
|
|
||||||
inset 0 0 0 1px rgba(59, 130, 246, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glare Effect */
|
|
||||||
.card-glare {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 1rem;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at var(--glare-x, 50%) var(--glare-y, 50%),
|
|
||||||
rgba(255, 255, 255, 0.15) 0%,
|
|
||||||
transparent 60%
|
|
||||||
);
|
|
||||||
opacity: var(--glare-opacity, 0);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
mix-blend-mode: overlay;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Floating Content inside Card */
|
|
||||||
.service-header {
|
.service-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.75rem;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
transform: translateZ(25px); /* Pop out */
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-card-content h3 {
|
.service-card h3 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.25rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-weight: 700;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
font-size: 1.2rem;
|
font-size: 1.5rem;
|
||||||
transform: translateZ(35px); /* Pop out more */
|
flex-shrink: 0;
|
||||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-description {
|
.service-description {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
transform: translateZ(15px); /* Pop out slightly */
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tech Section */
|
/* Tech Section */
|
||||||
.tech {
|
.tech {
|
||||||
background: linear-gradient(180deg, rgba(10, 14, 23, 0) 0%, var(--bg-dark) 100%);
|
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-grid {
|
.tech-grid {
|
||||||
|
|
@ -235,17 +227,6 @@ section {
|
||||||
.tech-item {
|
.tech-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
border-radius: 1rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-item:hover {
|
|
||||||
transform: translateY(-5px) scale(1.02);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-color: var(--border);
|
|
||||||
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-item h3 {
|
.tech-item h3 {
|
||||||
|
|
@ -265,8 +246,6 @@ section {
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: 3rem 0;
|
padding: 3rem 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer p {
|
.footer p {
|
||||||
|
|
@ -304,19 +283,174 @@ section {
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-title {
|
section {
|
||||||
font-size: 3rem;
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid,
|
||||||
|
.tech-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scroll Animations */
|
/* Smooth scrolling */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection color */
|
||||||
|
::selection {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll-triggered animations */
|
||||||
[data-animate] {
|
[data-animate] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(30px);
|
transform: translateY(30px);
|
||||||
transition: opacity 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
|
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-animate].animate-in {
|
[data-animate].animate-in {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Parallax effect on hero */
|
||||||
|
.hero::before {
|
||||||
|
animation: parallax-float 20s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes parallax-float {
|
||||||
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
|
50% { transform: translateY(-20px) scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated gradient text */
|
||||||
|
.hero-title {
|
||||||
|
background-size: 200% auto;
|
||||||
|
animation: gradient-shift 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient-shift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover micro-interactions */
|
||||||
|
.service-card {
|
||||||
|
transform-origin: center;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card:hover {
|
||||||
|
animation: subtle-bounce 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes subtle-bounce {
|
||||||
|
0%, 100% { transform: translateY(-4px); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-item {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-item:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-item:hover h3 {
|
||||||
|
animation: pulse-glow 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.8; text-shadow: 0 0 20px rgba(59, 130, 246, 0.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer links animation */
|
||||||
|
.footer-links a {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--accent);
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover::before {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero badge pulse */
|
||||||
|
.hero-badge {
|
||||||
|
animation: float 3s ease-in-out infinite, glow-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 10px rgba(59, 130, 246, 0.2); }
|
||||||
|
50% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.4); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badge animations */
|
||||||
|
.status-badge {
|
||||||
|
animation: badge-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes badge-pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section title entrance */
|
||||||
|
.section-title {
|
||||||
|
animation: slide-in-top 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-top {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce motion for accessibility */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue