|
3 | 3 |
|
4 | 4 | const body = document.body; |
5 | 5 | const base = (body?.getAttribute('data-base') || '.').trim(); |
6 | | - const assetVersion = '20260410f'; |
| 6 | + const assetVersion = '20260410g'; |
7 | 7 | const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; |
8 | 8 | const SETTLE_PASS_DELAYS = [0, 140, 320, 560]; |
9 | 9 | const simpleIcon = (name) => `https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/${name}.svg`; |
|
51 | 51 | "Diving into the core of operating systems and device environments. My work involves Custom ROM development and low-level system exploration, studying how device architectures function from the inside out to build highly optimized environments.", |
52 | 52 | arsenalKind: "engineering", |
53 | 53 | arsenal: [ |
54 | | - { iconSvg: iconSvg('<rect x="4.1" y="4.5" width="11.8" height="8.2" rx="1.8"/><path d="M6.5 15.5h7"/><path d="M8 12.7v2.8M12 12.7v2.8"/>'), label: "Custom ROM Building" }, |
| 54 | + { iconSvg: iconSvg('<path d="M6.1 7.2h7.8"/><path d="M7.3 5.1v2.1M12.7 5.1v2.1"/><path d="M6.8 7.2 5.7 12h8.6l-1.1-4.8"/><path d="M7.6 12v2.2M12.4 12v2.2"/><path d="M6.2 14.2h1.7M12.1 14.2h1.7"/>'), label: "Custom ROM Building" }, |
55 | 55 | { iconSvg: iconSvg('<path d="M10 4.2 14 5.7v3.8c0 2.6-1.6 4.8-4 5.9-2.4-1.1-4-3.3-4-5.9V5.7L10 4.2Z"/><path d="m12.7 12.7 2.6 2.6"/><circle cx="12.1" cy="12.1" r="2.3"/>'), label: "iOS Security Analysis" }, |
56 | 56 | { iconSvg: iconSvg('<path d="m6.4 6.2-3.1 3.8 3.1 3.8"/><path d="m13.6 6.2 3.1 3.8-3.1 3.8"/><path d="m11 4.8-2 10.4"/>'), label: "Reverse Engineering" }, |
57 | | - { iconSvg: iconSvg('<rect x="4.2" y="4.2" width="6.2" height="6.2" rx="1.3"/><rect x="9.6" y="9.6" width="6.2" height="6.2" rx="1.3"/><path d="M9.6 7.4h2.1M10.7 6.3v2.2"/>'), label: "System Virtualization" } |
| 57 | + { iconSvg: iconSvg('<rect x="4.1" y="4.5" width="11.8" height="8.2" rx="1.8"/><path d="M6.5 15.5h7"/><path d="M8 12.7v2.8M12 12.7v2.8"/>'), label: "System Virtualization" } |
58 | 58 | ] |
59 | 59 | }, |
60 | 60 | { |
|
190 | 190 | discipline.arsenal.forEach((item) => { |
191 | 191 | const pill = document.createElement("span"); |
192 | 192 | pill.className = "discipline-pill"; |
| 193 | + if (item.label) { |
| 194 | + pill.classList.add(`discipline-pill--${item.label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`); |
| 195 | + } |
193 | 196 |
|
194 | 197 | const icon = document.createElement("span"); |
195 | 198 | icon.className = "discipline-pill__icon"; |
|
202 | 205 | image.loading = "lazy"; |
203 | 206 | image.decoding = "async"; |
204 | 207 | image.referrerPolicy = "no-referrer"; |
| 208 | + image.draggable = false; |
205 | 209 | icon.appendChild(image); |
206 | 210 | } else if (item.iconSvg) { |
207 | 211 | icon.innerHTML = item.iconSvg; |
|
1582 | 1586 | const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); |
1583 | 1587 | const createLayout = (x, y, scale, rotate) => ({ x, y, scale, rotate }); |
1584 | 1588 | const getSide = (offset) => (offset === 0 ? "front" : offset < 0 ? "left" : "right"); |
| 1589 | + const getBaseCardHeight = () => { |
| 1590 | + const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; |
| 1591 | + if (portraitQuery.matches) { |
| 1592 | + return clamp(window.innerWidth * 0.84, 25.8 * rem, 30.4 * rem); |
| 1593 | + } |
| 1594 | + |
| 1595 | + if (window.matchMedia("(max-width: 980px) and (orientation: landscape)").matches) { |
| 1596 | + return clamp(window.innerWidth * 0.33, 18.8 * rem, 22.8 * rem); |
| 1597 | + } |
| 1598 | + |
| 1599 | + if (window.innerWidth <= 980) { |
| 1600 | + return clamp(window.innerWidth * 0.4, 20 * rem, 24 * rem); |
| 1601 | + } |
| 1602 | + |
| 1603 | + return clamp(window.innerWidth * 0.39, 22 * rem, 28 * rem); |
| 1604 | + }; |
1585 | 1605 |
|
1586 | 1606 | const buildLayouts = (cardWidth) => { |
1587 | 1607 | const steps = portraitQuery.matches |
1588 | 1608 | ? [ |
1589 | 1609 | { x: 0, scale: 1, rotate: 0 }, |
1590 | | - { x: cardWidth * 0.19, scale: 0.912, rotate: 4.2 }, |
1591 | | - { x: cardWidth * 0.302, scale: 0.828, rotate: 6.2 }, |
1592 | | - { x: cardWidth * 0.378, scale: 0.752, rotate: 7.5 }, |
1593 | | - { x: cardWidth * 0.428, scale: 0.688, rotate: 8.4 } |
| 1610 | + { x: cardWidth * 0.21, scale: 0.892, rotate: 4.9 }, |
| 1611 | + { x: cardWidth * 0.334, scale: 0.786, rotate: 7.8 }, |
| 1612 | + { x: cardWidth * 0.418, scale: 0.692, rotate: 10.2 }, |
| 1613 | + { x: cardWidth * 0.474, scale: 0.614, rotate: 12.2 } |
1594 | 1614 | ] |
1595 | 1615 | : [ |
1596 | 1616 | { x: 0, scale: 1, rotate: 0 }, |
1597 | | - { x: cardWidth * 0.214, scale: 0.924, rotate: 3.7 }, |
1598 | | - { x: cardWidth * 0.338, scale: 0.846, rotate: 5.1 }, |
1599 | | - { x: cardWidth * 0.424, scale: 0.772, rotate: 6.2 }, |
1600 | | - { x: cardWidth * 0.482, scale: 0.71, rotate: 7.0 } |
| 1617 | + { x: cardWidth * 0.224, scale: 0.904, rotate: 4.4 }, |
| 1618 | + { x: cardWidth * 0.356, scale: 0.8, rotate: 6.8 }, |
| 1619 | + { x: cardWidth * 0.446, scale: 0.706, rotate: 8.9 }, |
| 1620 | + { x: cardWidth * 0.506, scale: 0.626, rotate: 10.5 } |
1601 | 1621 | ]; |
1602 | 1622 |
|
1603 | 1623 | return steps.reduce((layouts, step, depth) => { |
|
1616 | 1636 | const formatTransform = (layout) => |
1617 | 1637 | `translate(calc(-50% + ${layout.x.toFixed(2)}px), ${layout.y.toFixed(2)}px) scale(${layout.scale.toFixed(4)}) rotate(${layout.rotate.toFixed(2)}deg)`; |
1618 | 1638 |
|
1619 | | - const interpolateLayout = (from, to, t) => ({ |
1620 | | - x: from.x + (to.x - from.x) * t, |
1621 | | - y: from.y + (to.y - from.y) * t, |
1622 | | - scale: from.scale + (to.scale - from.scale) * t, |
1623 | | - rotate: from.rotate + (to.rotate - from.rotate) * t |
1624 | | - }); |
1625 | | - |
1626 | 1639 | const getDepthAppearance = (offset) => { |
1627 | 1640 | const depth = Math.min(Math.abs(offset), total - 1); |
1628 | 1641 | const dim = [0.028, 0.11, 0.18, 0.24, 0.29][depth] || 0.29; |
|
1644 | 1657 |
|
1645 | 1658 | cards.forEach((card) => { |
1646 | 1659 | const surface = card.querySelector(".discipline-stack-card__surface"); |
1647 | | - const contentHeight = surface?.scrollHeight || card.scrollHeight || 0; |
| 1660 | + const header = surface?.querySelector(".discipline-stack-card__header"); |
| 1661 | + const body = surface?.querySelector(".discipline-stack-card__body"); |
| 1662 | + const arsenal = surface?.querySelector(".discipline-stack-card__arsenal:not([hidden])"); |
| 1663 | + const styles = surface ? getComputedStyle(surface) : null; |
| 1664 | + const gap = styles ? parseFloat(styles.rowGap || styles.gap) || 0 : 0; |
| 1665 | + const paddingTop = styles ? parseFloat(styles.paddingTop) || 0 : 0; |
| 1666 | + const paddingBottom = styles ? parseFloat(styles.paddingBottom) || 0 : 0; |
| 1667 | + const parts = [header, body, arsenal].filter(Boolean); |
| 1668 | + const contentHeight = Math.ceil( |
| 1669 | + paddingTop + |
| 1670 | + paddingBottom + |
| 1671 | + parts.reduce((sum, part) => sum + (part.scrollHeight || part.getBoundingClientRect().height || 0), 0) + |
| 1672 | + gap * Math.max(0, parts.length - 1) |
| 1673 | + ); |
1648 | 1674 | maxContentHeight = Math.max(maxContentHeight, contentHeight); |
1649 | 1675 | }); |
1650 | 1676 |
|
1651 | 1677 | const cardWidth = firstCard?.offsetWidth || stack.clientWidth || window.innerWidth; |
1652 | 1678 | const breathingRoom = Math.ceil(clamp(window.innerHeight * 0.04, 32, 56)); |
1653 | | - const currentHeight = firstCard?.offsetHeight || stack.clientHeight || 0; |
1654 | | - const cardHeight = Math.ceil(Math.max(currentHeight, maxContentHeight + breathingRoom)); |
| 1679 | + const baseHeight = Math.ceil(getBaseCardHeight()); |
| 1680 | + const cardHeight = Math.ceil(Math.max(baseHeight, maxContentHeight + breathingRoom)); |
1655 | 1681 | const pad = Math.ceil(Math.max(28, cardHeight * 0.08)); |
1656 | 1682 |
|
1657 | 1683 | shell?.style.setProperty("--discipline-card-width-resolved", `${cardWidth}px`); |
|
1702 | 1728 |
|
1703 | 1729 | const applyState = ({ dragProgress = 0 } = {}) => { |
1704 | 1730 | const isDragging = portraitQuery.matches && Math.abs(dragProgress) > 0.001; |
1705 | | - const travel = dragProgress === 0 ? 0 : dragProgress < 0 ? 1 : -1; |
| 1731 | + const dragSign = dragProgress === 0 ? 0 : Math.sign(dragProgress); |
| 1732 | + const currentMetrics = getMetrics(); |
| 1733 | + const dragMagnitude = Math.abs(dragProgress); |
1706 | 1734 |
|
1707 | 1735 | stack.classList.toggle("is-dragging", isDragging); |
1708 | 1736 |
|
1709 | 1737 | cards.forEach((card, index) => { |
1710 | 1738 | const offset = index - activeIndex; |
1711 | 1739 | let visual = getLayoutForOffset(offset); |
1712 | 1740 |
|
1713 | | - if (isDragging && travel !== 0) { |
1714 | | - const target = getLayoutForOffset(offset + travel); |
1715 | | - visual = interpolateLayout(visual, target, Math.abs(dragProgress)); |
| 1741 | + if (isDragging && offset === 0 && dragSign !== 0) { |
| 1742 | + visual = createLayout( |
| 1743 | + currentMetrics.cardWidth * 0.66 * dragMagnitude * dragSign, |
| 1744 | + 0, |
| 1745 | + 1 - dragMagnitude * 0.028, |
| 1746 | + dragSign * 10.5 * dragMagnitude |
| 1747 | + ); |
1716 | 1748 | } |
1717 | 1749 |
|
1718 | 1750 | const appearance = getDepthAppearance(offset); |
|
1743 | 1775 | const throwSign = direction > 0 ? -1 : 1; |
1744 | 1776 | const finalLayout = getLayoutForOffset(direction > 0 ? -1 : 1); |
1745 | 1777 | const midLayout = createLayout( |
1746 | | - throwSign * currentMetrics.cardWidth * (portraitQuery.matches ? 0.34 : 0.3), |
| 1778 | + throwSign * currentMetrics.cardWidth * (portraitQuery.matches ? 0.56 : 0.52), |
1747 | 1779 | 0, |
1748 | | - 0.972, |
1749 | | - throwSign * (portraitQuery.matches ? 9.8 : 8.2) |
| 1780 | + 0.968, |
| 1781 | + throwSign * (portraitQuery.matches ? 12.8 : 10.4) |
1750 | 1782 | ); |
1751 | 1783 | const tuckLayout = createLayout( |
1752 | | - finalLayout.x * 1.12, |
| 1784 | + finalLayout.x * 1.18, |
1753 | 1785 | 0, |
1754 | | - Math.min(0.985, finalLayout.scale * 1.01), |
1755 | | - finalLayout.rotate + throwSign * 1.1 |
| 1786 | + Math.min(0.982, finalLayout.scale * 1.012), |
| 1787 | + finalLayout.rotate + throwSign * 1.35 |
1756 | 1788 | ); |
1757 | 1789 |
|
1758 | 1790 | card.style.transition = "none"; |
|
1764 | 1796 | { transform: formatTransform(finalLayout) } |
1765 | 1797 | ], |
1766 | 1798 | { |
1767 | | - duration: portraitQuery.matches ? 860 : 740, |
1768 | | - easing: "cubic-bezier(0.2, 0.82, 0.22, 1)", |
| 1799 | + duration: portraitQuery.matches ? 920 : 820, |
| 1800 | + easing: "cubic-bezier(0.18, 0.86, 0.22, 1)", |
1769 | 1801 | fill: "both" |
1770 | 1802 | } |
1771 | 1803 | ); |
|
1863 | 1895 |
|
1864 | 1896 | event.preventDefault(); |
1865 | 1897 | const width = Math.max(stack.clientWidth, 1); |
1866 | | - const raw = deltaX / (width * 0.28); |
| 1898 | + const raw = deltaX / (width * 0.2); |
1867 | 1899 | const direction = raw === 0 ? 0 : raw > 0 ? -1 : 1; |
1868 | 1900 | const outOfBounds = (direction < 0 && activeIndex === 0) || (direction > 0 && activeIndex === total - 1); |
1869 | | - const limit = outOfBounds ? 0.18 : 0.74; |
1870 | | - const resistance = outOfBounds ? 2.2 : 1.08; |
| 1901 | + const limit = outOfBounds ? 0.2 : 0.92; |
| 1902 | + const resistance = outOfBounds ? 2.05 : 0.88; |
1871 | 1903 | const progress = clamp(Math.sign(raw || 0) * limit * (1 - Math.exp(-Math.abs(raw) * resistance)), -limit, limit); |
1872 | 1904 | pointerState.progress = progress; |
1873 | 1905 | applyState({ dragProgress: progress }); |
|
0 commit comments