feat: implement customizable Bitcoin logo with 3D rotation and interactive controls
All checks were successful
Deploy to Production / deploy (push) Successful in 4s
All checks were successful
Deploy to Production / deploy (push) Successful in 4s
This commit is contained in:
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
19
.gitea/workflows/build.yaml
Normal file
19
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build and deploy with Docker Compose
|
||||||
|
run: |
|
||||||
|
# Sprzątaj TYLKO swój projekt
|
||||||
|
docker compose down --remove-orphans || true
|
||||||
|
docker compose up -d --build
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Stage 1: Build the Vue application
|
||||||
|
FROM node:lts-alpine as build-stage
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Serve the application with Nginx
|
||||||
|
FROM nginx:stable-alpine as production-stage
|
||||||
|
|
||||||
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
bitcoin-logo:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3003:80"
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- npm_public
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm_public:
|
||||||
|
external: true
|
||||||
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/bitcoin-btc-logo.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>₿ Bitcoin 3D - Premium SVG Animation</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
15
nginx.conf
Normal file
15
nginx.conf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
1334
package-lock.json
generated
Normal file
1334
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "bitcoin-logo",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Premium 3D SVG rotating Bitcoin logo",
|
||||||
|
"author": "Grzegorz Kucmierz",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.25"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
public/bitcoin-btc-logo.svg
Normal file
15
public/bitcoin-btc-logo.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
viewBox="0 0 4091.27 4091.73"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||||
|
<g id="Layer_x0020_1">
|
||||||
|
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||||
|
<g id="_1421344023328">
|
||||||
|
<path fill="#F7931A" fill-rule="nonzero" d="M4030.06 2540.77c-273.24,1096.01 -1383.32,1763.02 -2479.46,1489.71 -1095.68,-273.24 -1762.69,-1383.39 -1489.33,-2479.31 273.12,-1096.13 1383.2,-1763.19 2479,-1489.95 1096.06,273.24 1763.03,1383.51 1489.76,2479.57l0.02 -0.02z"/>
|
||||||
|
<path fill="white" fill-rule="nonzero" d="M2947.77 1754.38c40.72,-272.26 -166.56,-418.61 -450,-516.24l91.95 -368.8 -224.5 -55.94 -89.51 359.09c-59.02,-14.72 -119.63,-28.59 -179.87,-42.34l90.16 -361.46 -224.36 -55.94 -92 368.68c-48.84,-11.12 -96.81,-22.11 -143.35,-33.69l0.26 -1.16 -309.59 -77.31 -59.72 239.78c0,0 166.56,38.18 163.05,40.53 90.91,22.69 107.35,82.87 104.62,130.57l-104.74 420.15c6.26,1.59 14.38,3.89 23.34,7.49 -7.49,-1.86 -15.46,-3.89 -23.73,-5.87l-146.81 588.57c-11.11,27.62 -39.31,69.07 -102.87,53.33 2.25,3.26 -163.17,-40.72 -163.17,-40.72l-111.46 256.98 292.15 72.83c54.35,13.63 107.61,27.89 160.06,41.3l-92.9 373.03 224.24 55.94 92 -369.07c61.26,16.63 120.71,31.97 178.91,46.43l-91.69 367.33 224.51 55.94 92.89 -372.33c382.82,72.45 670.67,43.24 791.83,-303.02 97.63,-278.78 -4.86,-439.58 -206.26,-544.44 146.69,-33.83 257.18,-130.31 286.64,-329.61l-0.07 -0.05zm-512.93 719.26c-69.38,278.78 -538.76,128.08 -690.94,90.29l123.28 -494.2c152.17,37.99 640.17,113.17 567.67,403.91zm69.43 -723.3c-63.29,253.58 -453.96,124.75 -580.69,93.16l111.77 -448.21c126.73,31.59 534.85,90.55 468.94,355.05l-0.02 0z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
317
src/App.vue
Normal file
317
src/App.vue
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import BitcoinLogo from './components/BitcoinLogo.vue'
|
||||||
|
|
||||||
|
// Modes: 'auto', 'interactive', 'controlled'
|
||||||
|
const activeMode = ref('interactive');
|
||||||
|
const activeEasing = ref('linear');
|
||||||
|
const activeDuration = ref(4);
|
||||||
|
const activeFriction = ref(0.98);
|
||||||
|
const activeSymbolColor = ref('#ffffff');
|
||||||
|
const activeSize = ref(250);
|
||||||
|
const activeStrokeWidth = ref(5);
|
||||||
|
const externalRotation = ref(0);
|
||||||
|
|
||||||
|
const easingOptions = [
|
||||||
|
'linear',
|
||||||
|
'ease',
|
||||||
|
'ease-in',
|
||||||
|
'ease-out',
|
||||||
|
'ease-in-out',
|
||||||
|
'cubic-bezier(0.68, -0.55, 0.265, 1.55)' // bouncy
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleRotationUpdate = (val) => {
|
||||||
|
if (activeMode.value !== 'controlled') {
|
||||||
|
externalRotation.value = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="glass-card">
|
||||||
|
<header>
|
||||||
|
<h1>Bitcoin 3D</h1>
|
||||||
|
<p>Premium 3D Bitcoin logo with multiple rotation modes.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<BitcoinLogo
|
||||||
|
:mode="activeMode"
|
||||||
|
:easing="activeEasing"
|
||||||
|
:duration="activeDuration"
|
||||||
|
:friction="activeFriction"
|
||||||
|
:symbolColor="activeSymbolColor"
|
||||||
|
:size="activeSize"
|
||||||
|
:strokeWidth="activeStrokeWidth"
|
||||||
|
v-model:rotation="externalRotation"
|
||||||
|
@update:rotation="handleRotationUpdate"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="global-controls">
|
||||||
|
<div class="color-control">
|
||||||
|
<span class="label">SYMBOL COLOR</span>
|
||||||
|
<input type="color" v-model="activeSymbolColor">
|
||||||
|
</div>
|
||||||
|
<div class="size-control">
|
||||||
|
<span class="label">SIZE</span>
|
||||||
|
<input type="range" v-model.number="activeSize" min="50" max="400" step="1">
|
||||||
|
<span class="value">{{ activeSize }}px</span>
|
||||||
|
</div>
|
||||||
|
<div class="size-control">
|
||||||
|
<span class="label">STROKE</span>
|
||||||
|
<input type="range" v-model.number="activeStrokeWidth" min="0" max="20" step="0.5">
|
||||||
|
<span class="value">{{ activeStrokeWidth }}px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mode-selector">
|
||||||
|
<label
|
||||||
|
v-for="mode in ['auto', 'interactive', 'controlled']"
|
||||||
|
:key="mode"
|
||||||
|
:class="{ active: activeMode === mode }"
|
||||||
|
>
|
||||||
|
<input type="radio" :value="mode" v-model="activeMode">
|
||||||
|
{{ mode.toUpperCase() }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeMode === 'auto'" class="control-panel">
|
||||||
|
<div class="duration-control">
|
||||||
|
<span class="label">SPEED</span>
|
||||||
|
<input type="range" v-model.number="activeDuration" min="0.5" max="10" step="0.1">
|
||||||
|
<span class="value">{{ activeDuration }}s</span>
|
||||||
|
</div>
|
||||||
|
<select v-model="activeEasing" class="easing-select">
|
||||||
|
<option v-for="option in easingOptions" :key="option" :value="option">
|
||||||
|
{{ option === 'cubic-bezier(0.68, -0.55, 0.265, 1.55)' ? 'BOUNCY' : option.toUpperCase() }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeMode === 'interactive'" class="control-panel">
|
||||||
|
<div class="duration-control">
|
||||||
|
<span class="label">INERTIA</span>
|
||||||
|
<input type="range" v-model.number="activeFriction" min="0.8" max="0.999" step="0.001">
|
||||||
|
<span class="value">{{ (activeFriction * 100).toFixed(1) }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeMode === 'controlled'" class="control-panel">
|
||||||
|
<input type="range" v-model.number="externalRotation" min="0" max="360" step="1">
|
||||||
|
<span class="value">{{ externalRotation }}°</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<div class="badge">Vue 3</div>
|
||||||
|
<div class="badge">SVG 3D</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
margin: 2rem 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 400px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-control .label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-control input[type="color"] {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-control input[type="color"]::-webkit-color-swatch {
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-control .label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-control input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
accent-color: #f7931a;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-control .value {
|
||||||
|
font-family: monospace;
|
||||||
|
min-width: 45px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector label {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector label.active {
|
||||||
|
background: #f7931a;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(247, 147, 26, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-control .label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
accent-color: #f7931a;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel .value {
|
||||||
|
font-family: monospace;
|
||||||
|
min-width: 45px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easing-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
appearance: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easing-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fbbf24;
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
h1 { font-size: 2.5rem; }
|
||||||
|
.glass-card { padding: 2rem; margin: 1rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
221
src/components/BitcoinLogo.vue
Normal file
221
src/components/BitcoinLogo.vue
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
|
||||||
|
const FIX_SHOWING_THROUGH = '0.5px';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// Modes: 'auto', 'interactive', 'controlled'
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'interactive'
|
||||||
|
},
|
||||||
|
easing: {
|
||||||
|
type: String,
|
||||||
|
default: 'linear'
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
default: 4
|
||||||
|
},
|
||||||
|
friction: {
|
||||||
|
type: Number,
|
||||||
|
default: 0.98
|
||||||
|
},
|
||||||
|
symbolColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#fff'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 200
|
||||||
|
},
|
||||||
|
strokeWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 5
|
||||||
|
},
|
||||||
|
// Used only in 'controlled' mode
|
||||||
|
rotation: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:rotation']);
|
||||||
|
|
||||||
|
// Internal state for 'interactive' mode
|
||||||
|
const internalRotation = ref(0);
|
||||||
|
const velocity = ref(2);
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const lastMouseX = ref(0);
|
||||||
|
|
||||||
|
// Final rotation value used in CSS
|
||||||
|
const displayRotation = computed(() => {
|
||||||
|
if (props.mode === 'controlled') return props.rotation;
|
||||||
|
return internalRotation.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rotationStyle = computed(() => `${displayRotation.value}deg`);
|
||||||
|
|
||||||
|
// Drag handlers
|
||||||
|
const handleStart = (e) => {
|
||||||
|
if (props.mode !== 'interactive') return;
|
||||||
|
isDragging.value = true;
|
||||||
|
lastMouseX.value = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMove = (e) => {
|
||||||
|
if (!isDragging.value || props.mode !== 'interactive') return;
|
||||||
|
const currentX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
|
||||||
|
const deltaX = currentX - lastMouseX.value;
|
||||||
|
|
||||||
|
internalRotation.value += deltaX * 0.5;
|
||||||
|
velocity.value = deltaX * 0.5;
|
||||||
|
lastMouseX.value = currentX;
|
||||||
|
|
||||||
|
emit('update:rotation', internalRotation.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnd = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
let rafId = null;
|
||||||
|
const animate = () => {
|
||||||
|
if (props.mode === 'interactive' && !isDragging.value) {
|
||||||
|
internalRotation.value += velocity.value;
|
||||||
|
velocity.value *= props.friction;
|
||||||
|
emit('update:rotation', internalRotation.value);
|
||||||
|
}
|
||||||
|
rafId = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
rafId = requestAnimationFrame(animate);
|
||||||
|
window.addEventListener('mousemove', handleMove);
|
||||||
|
window.addEventListener('mouseup', handleEnd);
|
||||||
|
window.addEventListener('touchmove', handleMove, { passive: false });
|
||||||
|
window.addEventListener('touchend', handleEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
window.removeEventListener('mousemove', handleMove);
|
||||||
|
window.removeEventListener('mouseup', handleEnd);
|
||||||
|
window.removeEventListener('touchmove', handleMove);
|
||||||
|
window.removeEventListener('touchend', handleEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync internal rotation if external control changes
|
||||||
|
watch(() => props.rotation, (newVal) => {
|
||||||
|
if (props.mode === 'controlled') {
|
||||||
|
internalRotation.value = newVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const coinShape = 'M98.4946664 62.0964188c-6.6779856,26.7864844 -33.808340799999996,43.0882088 -60.5980024,36.4085124 -26.778419200000002,-6.6779856 -43.0801436,-33.8100516 -36.3992252,-60.594336399999996 6.6750528000000005,-26.789417200000003 33.805408,-43.0923636 60.58676,-36.414378 26.787706399999998,6.6779856 43.0884532,33.8129844 36.4097344,60.6006908l0.0004888 -0.0004888z';
|
||||||
|
const symbolShape = 'M72.0434988 42.8770472c0.9951968,-6.6540344 -4.0707264,-10.2308284 -10.998,-12.6169056l2.247258 -9.013472 -5.4867799999999995 -1.3671735999999999 -2.1876244000000002 8.7761596c-1.4424488,-0.3597568 -2.9237572,-0.6987396 -4.3960228,-1.0347896l2.2035104 -8.8340824 -5.4833584 -1.3671735999999999 -2.24848 9.0105392c-1.1936496,-0.2717728 -2.3660364,-0.5403684 -3.5034739999999998,-0.8233836l0.0063544000000000005 -0.028350399999999998 -7.566379599999999 -1.8894564 -1.4595567999999999 5.8602232c0,0 4.0707264,0.9331192 3.984942,0.9905532 2.2218404,0.5545436 2.623634,2.0253428 2.5569128,3.1911308l-2.5598456 10.268466c0.1529944,0.0388596 0.3514472,0.0950716 0.5704296,0.1830556 -0.1830556,-0.0454584 -0.3778424,-0.0950716 -0.5799612,-0.1434628l-3.5880364 14.384650800000001c-0.2715284,0.6750328 -0.9607364,1.6880707999999998 -2.5141428,1.3033852 0.05499,0.07967439999999999 -3.9878747999999997,-0.9951968 -3.9878747999999997,-0.9951968l-2.7240824 6.280591200000001 7.140146 1.7799652c1.328314,0.3331172 2.6299884,0.6816316 3.9118664,1.009372l-2.270476 9.1168532 5.4804256 1.3671735999999999 2.24848 -9.0200708c1.4971944,0.4064372 2.9501524,0.7813468 4.3725604,1.1347492l-2.2409035999999998 8.9775452 5.4870244 1.3671735999999999 2.2702316 -9.0997452c9.3561208,1.770678 16.391174799999998,1.0567856 19.3523252,-7.4058088 2.3860772,-6.8133832 -0.1187784,-10.743335199999999 -5.0409944,-13.306113600000002 3.5851036,-0.8268051999999999 6.2854792,-3.1847764 7.0054815999999995,-8.0556684l-0.0017108000000000002 -0.001222zm-12.536009199999999 17.5787144c-1.6956471999999998,6.8133832 -13.1672944,3.1302752000000003 -16.886573600000002,2.2066876l3.0129632 -12.078248c3.7190347999999998,0.9284756000000001 15.645754799999999,2.7658748 13.873854799999998,9.8715604zm1.6968692 -17.677452c-1.5468076,6.1974952000000005 -11.0947824,3.04889 -14.192063600000001,2.2768303999999997l2.7316588 -10.9542524c3.0972812000000003,0.7720596 13.071734000000001,2.2130419999999997 11.4608936,8.677422l-0.0004888 0z';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="logo-container"
|
||||||
|
@mousedown="handleStart"
|
||||||
|
@touchstart="handleStart"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="coin"
|
||||||
|
:class="{
|
||||||
|
'auto-rotate': props.mode === 'auto',
|
||||||
|
'dragging': isDragging
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="side heads">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 100">
|
||||||
|
<path fill="#F7931A" fill-rule="nonzero" :stroke="symbolColor" :stroke-width="strokeWidth" :d="coinShape"></path>
|
||||||
|
<path :fill="symbolColor" fill-rule="nonzero" :d="symbolShape"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="side tails">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="svg_back" width="100%" height="100%" viewBox="0 0 100 100">
|
||||||
|
<path fill="#F7931A" fill-rule="nonzero" :stroke="symbolColor" :stroke-width="strokeWidth" :d="coinShape"></path>
|
||||||
|
<path :fill="symbolColor" fill-rule="nonzero" :d="symbolShape"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logo-container {
|
||||||
|
font-size: v-bind("props.size + 'px'");
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
/* background: rgba(255, 0, 0, 0.2); Bounding rect debug */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: v-bind("props.mode === 'interactive' ? (isDragging ? 'grabbing' : 'grab') : 'default'");
|
||||||
|
user-select: none;
|
||||||
|
touch-action: v-bind("props.mode === 'interactive' ? 'none' : 'auto'");
|
||||||
|
}
|
||||||
|
|
||||||
|
.coin {
|
||||||
|
width: 0.1em;
|
||||||
|
height: 1em;
|
||||||
|
background: linear-gradient(#faa504, #141001);
|
||||||
|
transform: rotateY(v-bind(rotationStyle));
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coin.auto-rotate {
|
||||||
|
animation: rotate v-bind("props.duration + 's'") infinite v-bind("props.easing");
|
||||||
|
}
|
||||||
|
|
||||||
|
.coin .side, .coin:before, .coin:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
right: calc(-0.4em + v-bind(FIX_SHOWING_THROUGH));
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
transform: rotateY(-90deg);
|
||||||
|
-moz-backface-visibility: hidden;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coin .tails, .coin:after {
|
||||||
|
left: calc(-0.4em + v-bind(FIX_SHOWING_THROUGH));
|
||||||
|
transform: rotateY(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coin:before, .coin:after {
|
||||||
|
background: linear-gradient(#faa504, #141001);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform: rotateY(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coin:after {
|
||||||
|
transform: rotateY(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg_back {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% { transform: rotateY(90deg); }
|
||||||
|
100% { transform: rotateY(450deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
src/main.js
Normal file
5
src/main.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
||||||
65
src/style.css
Normal file
65
src/style.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
:root {
|
||||||
|
font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: dark;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
background-color: #0f172a;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
/* Center vertically if content is small */
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: radial-gradient(circle at center, #1e293b 0%, #0f172a 100%);
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Ensure vertical scroll is allowed */
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: linear-gradient(to right, #f59e0b, #fbbf24);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
filter: drop-shadow(0 0 10px rgba(245, 158, 11, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
background: rgba(30, 41, 59, 0.5);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 3rem;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
15
vite.config.js
Normal file
15
vite.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
allowedHosts: true,
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Expires': '0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user