Compare commits
2 Commits
a81ec7901d
...
11d52b2e86
| Author | SHA1 | Date |
|---|---|---|
|
|
11d52b2e86 | |
|
|
db593cd299 |
|
|
@ -30,6 +30,9 @@ features = [
|
|||
"HtmlCanvasElement",
|
||||
"CanvasRenderingContext2d",
|
||||
"CanvasGradient",
|
||||
"MouseEvent",
|
||||
"Event",
|
||||
"CssStyleDeclaration",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ export interface InitOutput {
|
|||
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_3: WebAssembly.Table;
|
||||
readonly __wbindgen_export_4: (a: number, b: number) => void;
|
||||
readonly __wbindgen_export_5: (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, c: number) => void;
|
||||
readonly __wbindgen_export_6: (a: number, b: number) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -237,12 +237,16 @@ export function main() {
|
|||
wasm.main();
|
||||
}
|
||||
|
||||
function __wbg_adapter_4(arg0, arg1) {
|
||||
wasm.__wbindgen_export_4(arg0, arg1);
|
||||
function __wbg_adapter_4(arg0, arg1, arg2, arg3) {
|
||||
wasm.__wbindgen_export_4(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
||||
}
|
||||
|
||||
function __wbg_adapter_7(arg0, arg1, arg2, arg3) {
|
||||
wasm.__wbindgen_export_5(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
||||
function __wbg_adapter_11(arg0, arg1, arg2) {
|
||||
wasm.__wbindgen_export_5(arg0, arg1, addHeapObject(arg2));
|
||||
}
|
||||
|
||||
function __wbg_adapter_14(arg0, arg1) {
|
||||
wasm.__wbindgen_export_6(arg0, arg1);
|
||||
}
|
||||
|
||||
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
|
||||
|
|
@ -314,14 +318,25 @@ function __wbg_get_imports() {
|
|||
const ret = getObject(arg0).classList;
|
||||
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) {
|
||||
const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_createRadialGradient_b10566e092cb7089 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6) {
|
||||
const ret = getObject(arg0).createRadialGradient(arg1, arg2, arg3, arg4, arg5, arg6);
|
||||
imports.wbg.__wbg_createLinearGradient_098463fca2d0190f = function(arg0, arg1, arg2, arg3, arg4) {
|
||||
const ret = getObject(arg0).createLinearGradient(arg1, arg2, arg3, arg4);
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
};
|
||||
imports.wbg.__wbg_document_7d29d139bd619045 = function(arg0) {
|
||||
const ret = getObject(arg0).document;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
|
|
@ -339,6 +354,10 @@ function __wbg_get_imports() {
|
|||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, 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) {
|
||||
const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2));
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
|
|
@ -347,6 +366,10 @@ function __wbg_get_imports() {
|
|||
const ret = getObject(arg0)[arg1 >>> 0];
|
||||
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) {
|
||||
const ret = getObject(arg0).innerHeight;
|
||||
return addHeapObject(ret);
|
||||
|
|
@ -385,6 +408,16 @@ function __wbg_get_imports() {
|
|||
const ret = result;
|
||||
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) {
|
||||
let result;
|
||||
try {
|
||||
|
|
@ -413,6 +446,10 @@ function __wbg_get_imports() {
|
|||
const ret = getObject(arg0).item(arg1 >>> 0);
|
||||
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) {
|
||||
const ret = getObject(arg0).length;
|
||||
return ret;
|
||||
|
|
@ -421,9 +458,15 @@ function __wbg_get_imports() {
|
|||
const ret = getObject(arg0).length;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_lineTo_d9b895383c2303ba = function(arg0, arg1, arg2) {
|
||||
getObject(arg0).lineTo(arg1, arg2);
|
||||
};
|
||||
imports.wbg.__wbg_log_6c7b5f4f00b8ce3f = function(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() {
|
||||
const ret = new Object();
|
||||
return addHeapObject(ret);
|
||||
|
|
@ -443,10 +486,6 @@ function __wbg_get_imports() {
|
|||
const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2));
|
||||
return addHeapObject(ret);
|
||||
}, 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;
|
||||
|
|
@ -454,6 +493,9 @@ function __wbg_get_imports() {
|
|||
imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||
}, 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) {
|
||||
getObject(arg0).className = getStringFromWasm0(arg1, arg2);
|
||||
};
|
||||
|
|
@ -472,6 +514,12 @@ function __wbg_get_imports() {
|
|||
imports.wbg.__wbg_setinnerHTML_34e240d6b8e8260c = function(arg0, 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) {
|
||||
getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
|
||||
};
|
||||
|
|
@ -497,10 +545,21 @@ function __wbg_get_imports() {
|
|||
const ret = typeof window === 'undefined' ? null : window;
|
||||
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) {
|
||||
const ret = getObject(arg0).target;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_top_447ffcfee64c5d99 = function(arg0) {
|
||||
const ret = getObject(arg0).top;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_wbindgencbdrop_eb10308566512b88 = function(arg0) {
|
||||
const obj = getObject(arg0).original;
|
||||
if (obj.cnt-- == 1) {
|
||||
|
|
@ -533,6 +592,10 @@ function __wbg_get_imports() {
|
|||
imports.wbg.__wbg_wbindgenthrow_451ec1a8469d7eb6 = function(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) {
|
||||
// Cast intrinsic for `Ref(String) -> Externref`.
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
|
|
@ -540,7 +603,12 @@ function __wbg_get_imports() {
|
|||
};
|
||||
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_4);
|
||||
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_14);
|
||||
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);
|
||||
};
|
||||
imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) {
|
||||
|
|
@ -550,7 +618,7 @@ function __wbg_get_imports() {
|
|||
};
|
||||
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`.
|
||||
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_7);
|
||||
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_4);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -6,6 +6,7 @@ export const __wbindgen_export_0: (a: number) => void;
|
|||
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_3: WebAssembly.Table;
|
||||
export const __wbindgen_export_4: (a: number, b: number) => void;
|
||||
export const __wbindgen_export_5: (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, c: number) => void;
|
||||
export const __wbindgen_export_6: (a: number, b: number) => void;
|
||||
export const __wbindgen_start: () => void;
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
:root {
|
||||
--bg-dark: #0a0e17;
|
||||
--bg-card: #1a1f2e;
|
||||
--bg-card-hover: #252a3a;
|
||||
--bg-card: rgba(26, 31, 46, 0.8); /* More transparent for glass effect */
|
||||
--bg-card-hover: rgba(37, 42, 58, 0.9);
|
||||
--text-primary: #e0e6ed;
|
||||
--text-secondary: #9ca3af;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--rust: #f74c00;
|
||||
--border: #2d3748;
|
||||
--border: rgba(45, 55, 72, 0.5);
|
||||
--success: #10b981;
|
||||
--planned: #6366f1;
|
||||
}
|
||||
|
|
@ -38,37 +38,6 @@ body {
|
|||
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 {
|
||||
max-width: 1200px;
|
||||
|
|
@ -88,16 +57,6 @@ body {
|
|||
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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
|
@ -111,6 +70,7 @@ body {
|
|||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
filter: drop-shadow(0 0 2em rgba(59, 130, 246, 0.3));
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
|
|
@ -125,12 +85,14 @@ body {
|
|||
.hero-badge {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--bg-card);
|
||||
background: rgba(26, 31, 46, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2rem;
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
animation: float 3s ease-in-out infinite;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
|
@ -150,72 +112,118 @@ section {
|
|||
margin-bottom: 3rem;
|
||||
text-align: center;
|
||||
color: var(--text-primary);
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Services Section */
|
||||
.services {
|
||||
background: var(--bg-dark);
|
||||
/* Transparent to let particles show through slightly or keep dark */
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 2rem;
|
||||
perspective: 1000px; /* Global perspective for grid items entrance */
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
/* 3D Card System */
|
||||
.service-card-wrapper {
|
||||
/* This is the element that receives mouse events and defines the space */
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
perspective: 1500px; /* Perspective for the 3D tilt */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
.service-card-content {
|
||||
position: relative;
|
||||
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);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.4),
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.75rem;
|
||||
transform: translateZ(25px); /* Pop out */
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.service-card h3 {
|
||||
font-size: 1.25rem;
|
||||
.service-card-content h3 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
transform: translateZ(35px); /* Pop out more */
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
|
||||
}
|
||||
|
||||
.service-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
transform: translateZ(15px); /* Pop out slightly */
|
||||
}
|
||||
|
||||
/* Tech Section */
|
||||
.tech {
|
||||
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
|
||||
background: linear-gradient(180deg, rgba(10, 14, 23, 0) 0%, var(--bg-dark) 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tech-grid {
|
||||
|
|
@ -227,6 +235,17 @@ section {
|
|||
.tech-item {
|
||||
text-align: center;
|
||||
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 {
|
||||
|
|
@ -246,6 +265,8 @@ section {
|
|||
border-top: 1px solid var(--border);
|
||||
padding: 3rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
|
|
@ -283,174 +304,19 @@ section {
|
|||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.services-grid,
|
||||
.tech-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Scroll-triggered animations */
|
||||
/* Scroll Animations */
|
||||
[data-animate] {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
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);
|
||||
}
|
||||
|
||||
[data-animate].animate-in {
|
||||
opacity: 1;
|
||||
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,185 +3,327 @@ use wasm_bindgen::JsCast;
|
|||
use web_sys::{
|
||||
console, Document, Element, HtmlElement, Window,
|
||||
IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit,
|
||||
HtmlCanvasElement, CanvasRenderingContext2d,
|
||||
HtmlCanvasElement, CanvasRenderingContext2d, MouseEvent, CssStyleDeclaration,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
// Particle structure
|
||||
struct Particle {
|
||||
// Mouse State
|
||||
#[derive(Clone, Copy)]
|
||||
struct MouseState {
|
||||
x: f64,
|
||||
y: f64,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
size: f64,
|
||||
life: f64,
|
||||
max_life: f64,
|
||||
hue: f64,
|
||||
// Normalized coordinates (-1.0 to 1.0)
|
||||
norm_x: f64,
|
||||
norm_y: f64,
|
||||
}
|
||||
|
||||
impl Particle {
|
||||
fn new(x: f64, y: f64, hue: f64) -> Self {
|
||||
impl MouseState {
|
||||
fn new() -> 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 {
|
||||
x,
|
||||
y,
|
||||
vx: 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,
|
||||
vertices,
|
||||
edges,
|
||||
rotation_x: 0.0,
|
||||
rotation_y: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, width: f64, height: f64, time: f64) {
|
||||
// Multiple layered flow fields for complex fluid motion
|
||||
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();
|
||||
fn rotate(&mut self, dx: f64, dy: f64) {
|
||||
self.rotation_x += dx;
|
||||
self.rotation_y += dy;
|
||||
}
|
||||
}
|
||||
|
||||
// Particle system
|
||||
struct ParticleSystem {
|
||||
particles: Vec<Particle>,
|
||||
// Wave layer for fluid simulation
|
||||
struct WaveLayer {
|
||||
amplitude: f64,
|
||||
frequency: f64,
|
||||
speed: f64,
|
||||
offset: f64,
|
||||
}
|
||||
|
||||
// Fluid wave system
|
||||
struct FluidWaves {
|
||||
width: f64,
|
||||
height: f64,
|
||||
max_particles: usize,
|
||||
time: f64,
|
||||
layers: Vec<WaveLayer>,
|
||||
polyhedron: Polyhedron,
|
||||
mouse: Rc<RefCell<MouseState>>,
|
||||
}
|
||||
|
||||
impl ParticleSystem {
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
impl FluidWaves {
|
||||
fn new(width: f64, height: f64, mouse: Rc<RefCell<MouseState>>) -> 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 {
|
||||
particles: Vec::new(),
|
||||
width,
|
||||
height,
|
||||
max_particles: 1200,
|
||||
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));
|
||||
}
|
||||
layers,
|
||||
polyhedron: Polyhedron::new_dodecahedron(100.0),
|
||||
mouse,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// Rotate polyhedron based on mouse position and time
|
||||
let mouse = self.mouse.borrow();
|
||||
self.polyhedron.rotate(0.005 + mouse.norm_y * 0.01, 0.005 + mouse.norm_x * 0.01);
|
||||
}
|
||||
|
||||
// Remove dead particles
|
||||
self.particles.retain(|p| !p.is_dead());
|
||||
// Calculate wave height at a given position with randomness
|
||||
fn wave_height(&self, x: f64, y: f64, randomness: f64) -> f64 {
|
||||
let mut height = 0.0;
|
||||
|
||||
// Spawn new particles
|
||||
let spawn_count = (self.max_particles - self.particles.len()).min(5);
|
||||
self.spawn_particles(spawn_count);
|
||||
for layer in &self.layers {
|
||||
// Multiple sine waves at different frequencies for organic curves
|
||||
let wave1 = (x * layer.frequency + self.time * layer.speed + layer.offset).sin();
|
||||
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) {
|
||||
for particle in &self.particles {
|
||||
particle.draw(ctx);
|
||||
// Draw smooth gradient waves at the bottom
|
||||
self.draw_smooth_waves(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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -198,12 +340,34 @@ pub fn main() -> Result<(), JsValue> {
|
|||
let window = web_sys::window().expect("no global window exists");
|
||||
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
|
||||
if let Some(body) = document.body() {
|
||||
body.set_inner_html("");
|
||||
|
||||
// Create and setup particle canvas
|
||||
setup_particle_canvas(&document, &body, &window)?;
|
||||
setup_particle_canvas(&document, &body, &window, mouse_state.clone())?;
|
||||
|
||||
build_page(&document, &body)?;
|
||||
setup_scroll_animations(&document)?;
|
||||
|
|
@ -213,7 +377,12 @@ pub fn main() -> Result<(), JsValue> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_particle_canvas(document: &Document, body: &HtmlElement, window: &Window) -> Result<(), JsValue> {
|
||||
fn setup_particle_canvas(
|
||||
document: &Document,
|
||||
body: &HtmlElement,
|
||||
window: &Window,
|
||||
mouse_state: Rc<RefCell<MouseState>>
|
||||
) -> Result<(), JsValue> {
|
||||
console::log_1(&"🎨 Setting up particle canvas...".into());
|
||||
|
||||
// Create canvas element
|
||||
|
|
@ -240,47 +409,24 @@ fn setup_particle_canvas(document: &Document, body: &HtmlElement, window: &Windo
|
|||
.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(600);
|
||||
console::log_1(&format!("🌟 Spawned {} initial particles", particle_system.borrow().particles.len()).into());
|
||||
// Initialize fluid wave system
|
||||
let wave_system = Rc::new(RefCell::new(FluidWaves::new(width, height, mouse_state)));
|
||||
console::log_1(&"🌊 Fluid wave system initialized!".into());
|
||||
|
||||
// Setup animation loop
|
||||
let system_clone = particle_system.clone();
|
||||
let system_clone = wave_system.clone();
|
||||
let context_clone = context.clone();
|
||||
let frame_count = Rc::new(RefCell::new(0u32));
|
||||
let window_clone = window.clone();
|
||||
|
||||
let animate_closure = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
|
||||
let animate_clone = animate_closure.clone();
|
||||
let frame_clone = frame_count.clone();
|
||||
|
||||
*animate_closure.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
||||
*frame_clone.borrow_mut() += 1;
|
||||
|
||||
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)");
|
||||
// Clear canvas
|
||||
context_clone.set_fill_style_str("rgba(10, 14, 23, 1.0)");
|
||||
context_clone.fill_rect(0.0, 0.0, width, height);
|
||||
|
||||
// Update and draw particles
|
||||
// Update and draw waves
|
||||
system_clone.borrow_mut().update();
|
||||
system_clone.borrow().draw(&context_clone);
|
||||
|
||||
|
|
@ -299,12 +445,12 @@ fn setup_particle_canvas(document: &Document, body: &HtmlElement, window: &Windo
|
|||
|
||||
// Setup resize handler
|
||||
let canvas_clone = canvas.clone();
|
||||
let system_resize = particle_system.clone();
|
||||
let window_clone = window.clone();
|
||||
let system_resize = wave_system.clone();
|
||||
let window_resize = 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);
|
||||
let new_width = window_resize.inner_width().unwrap().as_f64().unwrap_or(800.0);
|
||||
let new_height = window_resize.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);
|
||||
|
|
@ -422,7 +568,7 @@ fn create_service_card(
|
|||
status: &str,
|
||||
url: Option<&str>,
|
||||
) -> Result<Element, JsValue> {
|
||||
let card = if let Some(link) = url {
|
||||
let wrapper = if let Some(link) = url {
|
||||
let a = document.create_element("a")?;
|
||||
a.set_attribute("href", link)?;
|
||||
a.set_attribute("target", "_blank")?;
|
||||
|
|
@ -431,7 +577,81 @@ fn create_service_card(
|
|||
} else {
|
||||
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")?;
|
||||
header.set_class_name("service-header");
|
||||
|
|
@ -450,10 +670,12 @@ fn create_service_card(
|
|||
desc.set_text_content(Some(description));
|
||||
desc.set_class_name("service-description");
|
||||
|
||||
card.append_child(&header)?;
|
||||
card.append_child(&desc)?;
|
||||
content.append_child(&header)?;
|
||||
content.append_child(&desc)?;
|
||||
|
||||
Ok(card)
|
||||
wrapper.append_child(&content)?;
|
||||
|
||||
Ok(wrapper)
|
||||
}
|
||||
|
||||
fn create_tech_section(document: &Document) -> Result<Element, JsValue> {
|
||||
|
|
|
|||
328
style.css
328
style.css
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
:root {
|
||||
--bg-dark: #0a0e17;
|
||||
--bg-card: #1a1f2e;
|
||||
--bg-card-hover: #252a3a;
|
||||
--bg-card: rgba(26, 31, 46, 0.8); /* More transparent for glass effect */
|
||||
--bg-card-hover: rgba(37, 42, 58, 0.9);
|
||||
--text-primary: #e0e6ed;
|
||||
--text-secondary: #9ca3af;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--rust: #f74c00;
|
||||
--border: #2d3748;
|
||||
--border: rgba(45, 55, 72, 0.5);
|
||||
--success: #10b981;
|
||||
--planned: #6366f1;
|
||||
}
|
||||
|
|
@ -38,37 +38,6 @@ body {
|
|||
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 {
|
||||
max-width: 1200px;
|
||||
|
|
@ -88,16 +57,6 @@ body {
|
|||
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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
|
@ -111,6 +70,7 @@ body {
|
|||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
filter: drop-shadow(0 0 2em rgba(59, 130, 246, 0.3));
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
|
|
@ -125,12 +85,14 @@ body {
|
|||
.hero-badge {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--bg-card);
|
||||
background: rgba(26, 31, 46, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2rem;
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
animation: float 3s ease-in-out infinite;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
|
@ -150,72 +112,118 @@ section {
|
|||
margin-bottom: 3rem;
|
||||
text-align: center;
|
||||
color: var(--text-primary);
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Services Section */
|
||||
.services {
|
||||
background: var(--bg-dark);
|
||||
/* Transparent to let particles show through slightly or keep dark */
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 2rem;
|
||||
perspective: 1000px; /* Global perspective for grid items entrance */
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
/* 3D Card System */
|
||||
.service-card-wrapper {
|
||||
/* This is the element that receives mouse events and defines the space */
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
perspective: 1500px; /* Perspective for the 3D tilt */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
.service-card-content {
|
||||
position: relative;
|
||||
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);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.4),
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.75rem;
|
||||
transform: translateZ(25px); /* Pop out */
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.service-card h3 {
|
||||
font-size: 1.25rem;
|
||||
.service-card-content h3 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
transform: translateZ(35px); /* Pop out more */
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
|
||||
}
|
||||
|
||||
.service-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
transform: translateZ(15px); /* Pop out slightly */
|
||||
}
|
||||
|
||||
/* Tech Section */
|
||||
.tech {
|
||||
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
|
||||
background: linear-gradient(180deg, rgba(10, 14, 23, 0) 0%, var(--bg-dark) 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tech-grid {
|
||||
|
|
@ -227,6 +235,17 @@ section {
|
|||
.tech-item {
|
||||
text-align: center;
|
||||
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 {
|
||||
|
|
@ -246,6 +265,8 @@ section {
|
|||
border-top: 1px solid var(--border);
|
||||
padding: 3rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
|
|
@ -283,174 +304,19 @@ section {
|
|||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.services-grid,
|
||||
.tech-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Scroll-triggered animations */
|
||||
/* Scroll Animations */
|
||||
[data-animate] {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
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);
|
||||
}
|
||||
|
||||
[data-animate].animate-in {
|
||||
opacity: 1;
|
||||
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