compose elt v0.1
Some checks failed
Deploy App / build (push) Has been cancelled
Deploy App / deploy (push) Has been cancelled

This commit is contained in:
valere
2025-12-23 12:55:37 +01:00
parent 8efafc4642
commit 2c826e29ea
16 changed files with 202 additions and 429 deletions

View File

@@ -11,10 +11,6 @@
<deckPlaylist :box="box" class="box-page" v-if="box.type === 'playlist'" @click.stop="" />
</template>
</box>
<nav class="fixed top-0">
<a class="button m-4 bg-esyellow" href="/draggable">Draggable</a>
<a class="button m-4 bg-esyellow" href="/mix">Mix</a>
</nav>
</div>
</template>

View File

@@ -1,36 +1,49 @@
<template>
<div class="bucket" :class="{ 'drag-over': isDragOver }" @dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
<div class="bucket-content">
<div v-if="cards.length === 0" class="bucket-empty">
<div v-if="tracks.length === 0" class="bucket-empty">
Drop cards here
</div>
<div v-else class="bucket-cards">
<Card v-for="(card, index) in cards" :key="index" :track="card" class="bucket-card" isFaceUp />
</div>
</div>
<draggable v-else v-model="tracks" item-key="id" class="bucket-cards" @start="onDragStart" @end="onDragEnd"
:touch-start-threshold="50">
<template #item="{ element: track }">
<card :track="track" tabindex="0" is-face-up class="bucket-card" @click="flipCard(track)" />
</template>
</draggable>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import draggable from 'vuedraggable'
import { useDataStore } from '~/store/data'
const props = defineProps<{
modelValue?: any[]
boxId?: string
}>()
const emit = defineEmits(['update:modelValue', 'card-dropped'])
const dataStore = useDataStore()
const isDragOver = ref(false)
const cards = ref<any[]>(props.modelValue || [])
const drag = ref(false)
const tracks = ref<any[]>(props.modelValue || [])
const touchedCard = ref<any>(null)
const touchStartPos = ref<{ x: number, y: number } | null>(null)
// Mettre à jour les cartes quand la prop change
watch(() => props.modelValue, (newValue) => {
if (newValue) {
cards.value = [...newValue]
tracks.value = [...newValue]
}
})
if (props.boxId) {
onMounted(async () => {
await dataStore.loadData()
tracks.value = dataStore.getTracksByboxId(props.boxId)
})
}
// Gestion du drag and drop desktop
const onDragEnter = (e: DragEvent) => {
e.preventDefault()
isDragOver.value = true
@@ -45,76 +58,79 @@ const onDragLeave = () => {
isDragOver.value = false
}
const onDragStart = () => {
drag.value = true
}
const onDragEnd = () => {
drag.value = false
isDragOver.value = false
}
const onDrop = (e: DragEvent) => {
isDragOver.value = false
// Récupérer les données de la carte depuis l'événement de glisser
const cardData = e.dataTransfer?.getData('application/json')
if (cardData) {
try {
const card = JSON.parse(cardData)
cards.value = [...cards.value, card]
emit('update:modelValue', cards.value)
emit('card-dropped', card)
const track = JSON.parse(cardData)
if (!tracks.value.some(t => t.id === track.id)) {
tracks.value.push(track)
}
} catch (e) {
console.error('Invalid card data', e)
console.error('Erreur lors du traitement de la carte déposée', e)
}
}
}
const flipCard = (track: any) => {
track.isFaceUp = !track.isFaceUp
}
</script>
<style scoped>
.bucket {
width: 120px;
min-height: 160px;
min-height: 200px;
border: 2px dashed #ccc;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
transition: all 0.2s ease;
padding: 1rem;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
background-color: rgba(255, 255, 255, 0.1);
touch-action: none;
}
.bucket.drag-over {
border-color: #4f46e5;
background-color: #eef2ff;
transform: scale(1.02);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.bucket-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
border-color: #4CAF50;
background-color: rgba(76, 175, 80, 0.1);
}
.bucket-empty {
color: #9ca3af;
text-align: center;
padding: 1rem;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #666;
font-style: italic;
}
.bucket-cards {
display: flex;
flex-direction: column;
gap: 0.5rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
width: 100%;
}
.bucket-card {
background: white;
border-radius: 4px;
padding: 0.5rem;
font-size: 0.8rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: transform 0.2s;
cursor: move;
touch-action: none;
/* Important pour le touch */
}
.bucket-card:hover {
transform: translateY(-4px);
}
.bucket-card:active {
opacity: 0.7;
}
</style>

View File

@@ -71,22 +71,25 @@ import type { Track } from '~~/types/types'
import { usePlayerStore } from '~/store/player'
import { useDataStore } from '~/store/data';
const props = withDefaults(defineProps<{ track: Track; isFaceUp?: boolean }>(), {
isFaceUp: false
const props = withDefaults(defineProps<{
track?: Track;
isFaceUp?: boolean;
}>(), {
track: () => {
const dataStore = useDataStore();
return dataStore.getRandomPlaylistTrack() || {} as Track;
},
isFaceUp: true,
})
const playerStore = usePlayerStore()
const isManifesto = computed(() => props.track.boxId.startsWith('ES00'))
const isOrder = computed(() => props.track.order && !isManifesto)
const isPlaylistTrack = computed(() => props.track.type === 'playlist')
const isRedCard = computed(() => props.track.card?.suit === '♥' || props.track.card?.suit === '♦')
const isRedCard = computed(() => (props.track.card?.suit === '♥' || props.track.card?.suit === '♦'))
const dataStore = useDataStore()
const cardColor = computed(() => dataStore.getYearColor(props.track.year || 0))
const coverUrl = computed(() => {
if (!props.track.coverId) return ''
return props.track.coverId.startsWith('http')
? props.track.coverId
: `https://f4.bcbits.com/img/${props.track.coverId}_4.jpg`
})
const coverUrl = computed(() => props.track.coverId || '/card-dock.svg')
const dragStart = (event: DragEvent) => {
if (event.dataTransfer) {

View File

@@ -1,11 +0,0 @@
<template>
<form
class="h-screen flex justify-center items-center flex-col absolute top-0 left-1/2 -translate-x-1/2"
>
<label for="email" class="block text-xl"> be notify when's evilSpins open : </label>
<div>
<input id="email" type="email" name="" placeholder="email" >
<button>ok</button>
</div>
</form>
</template>

View File

@@ -1,6 +1,7 @@
<template>
<div class="platine relative">
<div class="disc" :style="{ 'background-image': `url(${track?.coverId})` }" ref="discRef" id="disc">
<div class="platine" :class="{ 'drag-over': isDragOver }" @dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
<div class="disc" ref="discRef" :style="'background-image: url(' + coverUrl + ')'" id="disc">
<div
class="bobine bg-slate-800 bg-opacity-50 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full"
:style="{ height: progressPercentage + '%', width: progressPercentage + '%' }">
@@ -26,14 +27,19 @@ import Disc from '@/platine-tools/disc'
import Sampler from '@/platine-tools/sampler'
import type { Track } from '~~/types/types'
const props = withDefaults(defineProps<{ track: Track | undefined }>(), {})
const props = withDefaults(defineProps<{ track?: Track }>(), {})
const discRef = ref<HTMLElement>()
const currentTurns = ref(0)
const totalTurns = ref(0)
const progressPercentage = ref(0)
const isPlaying = ref(false)
const isLoadingTrack = ref(false)
const drag = ref(false)
const isDragOver = ref(false)
const isFirstDrag = ref(true)
const isLoadingTrack = ref(false)
const isPlaying = ref(false)
const coverUrl = computed(() => props.track?.coverId || '/card-dock.svg')
let disc: Disc | null = null
let sampler: Sampler | null = null
@@ -79,12 +85,12 @@ const handleRewind = () => {
}
};
const loadTrack = async () => {
if (!sampler || !props.track) return
const loadTrack = async (track: Track) => {
if (!sampler || !track) return
isLoadingTrack.value = true
try {
await sampler.loadTrack(props.track.url)
await sampler.loadTrack(track.url)
if (disc) {
disc.setDuration(sampler.duration)
updateTurns(disc)
@@ -96,6 +102,44 @@ const loadTrack = async () => {
}
}
// Gestion du drag and drop
const onDragEnter = (e: DragEvent) => {
e.preventDefault()
isDragOver.value = true
}
const onDragOver = (e: DragEvent) => {
e.preventDefault()
isDragOver.value = true
}
const onDragLeave = () => {
isDragOver.value = false
}
const onDragStart = () => {
drag.value = true
}
const onDragEnd = () => {
drag.value = false
isDragOver.value = false
}
const onDrop = (e: DragEvent) => {
isDragOver.value = false
const cardData = e.dataTransfer?.getData('application/json')
if (cardData) {
try {
const newTrack = JSON.parse(cardData)
if (newTrack && newTrack.url) {
loadTrack(newTrack)
}
} catch (error) {
console.error('Erreur lors du traitement de la carte déposée', error)
}
}
}
onMounted(async () => {
disc = new Disc(discRef.value!)
sampler = new Sampler()
@@ -131,8 +175,10 @@ onMounted(async () => {
})
watch(() => props.track, () => {
loadTrack()
watch(() => props.track, (newTrack) => {
if (newTrack) {
loadTrack(newTrack)
}
})
onUnmounted(() => {
@@ -149,12 +195,15 @@ onUnmounted(() => {
<style lang="scss">
.platine {
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
padding: 20px;
}
.disc {
position: relative;
background-color: white;
aspect-ratio: 1;
width: 100%;
overflow: hidden;
@@ -162,6 +211,11 @@ onUnmounted(() => {
cursor: grab;
background-position: center;
background-size: cover;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
.dragoOver & {
background-color: #4CAF50;
}
}
.disc.is-scratching {

View File

@@ -1,11 +1,9 @@
<template>
<div class="deck-playlist">
<div class="flex flex-col fixed right-0 top-0 z-50">
<button @click="closeDatBox" v-if="uiStore.isBoxSelected"
class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors z-50" aria-label="close the box">
close
</button>
<Bucket v-model="bucketCards" @card-dropped="onCardDropped" />
</div>
<div class="controls flex justify-center z-50 relative">
<button class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors" @click="toggleAllCards">
@@ -19,11 +17,14 @@
<card v-for="(track, i) in filteredTracks" :key="track.id" :track="track" :tabindex="i"
:is-face-up="isCardRevealed(track.id)" />
</div>
<div class="dock">
<Bucket @card-dropped="onCardDropped" />
<Platine />
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, ref } from 'vue'
import { useDataStore } from '~/store/data'
import { useCardStore } from '~/store/card'
import { usePlayerStore } from '~/store/player'
@@ -45,7 +46,6 @@ const uiStore = useUiStore()
const deck = ref()
const tracks = computed(() => dataStore.getTracksByboxId(props.box.id))
const filteredTracks = ref(tracks.value)
const bucketCards = ref([])
// Variables réactives pour les filtres
const selectedSuit = ref('')

View File

@@ -1,8 +1,3 @@
<template>
<div class="w-full flex flex-col items-center">
<slot />
<searchModal />
<loader />
<Player />
</div>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="w-full flex flex-col items-center">
<slot />
</div>
</template>

27
app/pages/boxes.vue Normal file
View File

@@ -0,0 +1,27 @@
<template>
<boxes />
</template>
<script setup>
import { useUiStore } from '~/store/ui'
import { useDataStore } from '~/store/data'
// Configuration du layout
definePageMeta({
layout: 'default'
})
const uiStore = useUiStore()
onMounted(async () => {
const dataStore = useDataStore()
await dataStore.loadData()
uiStore.listBoxes()
})
</script>
<style>
.logo {
filter: drop-shadow(3px 3px 0 rgb(0 0 0 / 0.7));
}
</style>

View File

@@ -1,169 +0,0 @@
<template>
<div>
<button
class="fixed bottom-4 right-4 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full shadow-lg z-50 transition-colors"
title="Ranger les cartes" @click="arrangeCards">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
</svg>
</button>
<div ref="deck" class="deck">
<card v-for="track in tracks" :key="track.id" :track="track"
:class="['card', 'id-' + track.id, { dragging: dragging === track }]" :style="{
top: track.y + 'px',
left: track.x + 'px',
zIndex: track.zIndex || 1,
transform: `rotate(${track.rotation || 0}deg)`
}" :is-face-up="track.isFaceUp" @mousedown="startDrag($event, track)" @click="flipCard(track)" />
</div>
</div>
</template>
<script setup>
import { useDataStore } from '~/store/data'
const tracks = ref([])
const deck = ref(null)
let zIndexCounter = 1
let dragging = null
let offset = { x: 0, y: 0 }
let velocity = { x: 0, y: 0 }
let lastTime = 0
const dataStore = useDataStore()
definePageMeta({ layout: 'default' })
onMounted(async () => {
await dataStore.loadData()
tracks.value = dataStore.getTracksByboxId('ESPLAYLIST').map((t, i) => ({
...t,
x: t.x ?? 50 + i * 20,
y: t.y ?? 50 + i * 20,
zIndex: t.zIndex ?? i + 1,
rotation: 0,
isFaceUp: t.isFaceUp ?? false
}))
window.addEventListener('mousemove', onDrag)
window.addEventListener('mouseup', stopDrag)
})
onBeforeUnmount(() => {
window.removeEventListener('mousemove', onDrag)
window.removeEventListener('mouseup', stopDrag)
})
function startDrag(e, track) {
dragging = track
const rect = deck.value.getBoundingClientRect()
offset.x = e.clientX - rect.left - track.x
offset.y = e.clientY - rect.top - track.y
lastTime = performance.now()
velocity = { x: 0, y: 0 }
zIndexCounter += 1
track.zIndex = zIndexCounter
}
function onDrag(e) {
if (!dragging) return
const rect = deck.value.getBoundingClientRect()
const newX = e.clientX - rect.left - offset.x
const newY = e.clientY - rect.top - offset.y
const now = performance.now()
const dt = now - lastTime
velocity.x = (newX - dragging.x) / dt
velocity.y = (newY - dragging.y) / dt
lastTime = now
dragging.x = newX
dragging.y = newY
// Rotation dynamique selon la position horizontale
const centerX = rect.width / 2
const dx = dragging.x - centerX
dragging.rotation = dx * 0.05
}
function stopDrag() {
if (!dragging) return
const track = dragging
dragging = null
// Inertie douce
const decay = 0.95
function animateInertia() {
if (Math.abs(velocity.x) < 0.02 && Math.abs(velocity.y) < 0.02) return
track.x += velocity.x * 16
track.y += velocity.y * 16
velocity.x *= decay
velocity.y *= decay
requestAnimationFrame(animateInertia)
}
animateInertia()
}
function flipCard(track) {
track.isFaceUp = true
}
function arrangeCards() {
const deckRect = deck.value.getBoundingClientRect()
const cardWidth = 224
const cardHeight = 320
const padding = 20
const cardsPerRow = Math.max(
1,
Math.floor((deckRect.width - padding * 2) / (cardWidth + padding))
)
tracks.value.forEach((track, index) => {
const row = Math.floor(index / cardsPerRow)
const col = index % cardsPerRow
track.x = padding + col * (cardWidth + padding)
track.y = padding + row * (cardHeight / 3)
track.zIndex = index + 1
track.rotation = 0
zIndexCounter = Math.max(zIndexCounter, track.zIndex)
})
}
</script>
<style lang="scss" scoped>
.deck {
position: relative;
width: 100%;
min-height: 80vh;
height: 100%;
overflow-y: auto;
padding: 1rem;
box-sizing: border-box;
}
.card {
position: absolute;
cursor: grab;
transition:
transform 0.1s ease,
box-shadow 0.2s ease;
}
.card:active {
cursor: grabbing;
}
.card.dragging {
z-index: 999;
cursor: grabbing;
transform: scale(1.05) rotate(0deg);
/* rotation sera remplacée dynamiquement */
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5);
}
</style>

View File

@@ -1,27 +1,4 @@
<template>
<boxes />
<card />
<platine />
</template>
<script setup>
import { useUiStore } from '~/store/ui'
import { useDataStore } from '~/store/data'
// Configuration du layout
definePageMeta({
layout: 'default'
})
const uiStore = useUiStore()
onMounted(async () => {
const dataStore = useDataStore()
await dataStore.loadData()
uiStore.listBoxes()
})
</script>
<style>
.logo {
filter: drop-shadow(3px 3px 0 rgb(0 0 0 / 0.7));
}
</style>

View File

@@ -1,78 +0,0 @@
<template>
<div class="flex flex-wrap justify-center items-center h-screen">
<div class="bg-page-dark-bg text-white">
<div class="flex flex-col-reverse bg-gradient-to-r from-primary to-primary-dark">
<div class="mt-8 flex flex-wrap justify-center">
<!-- <box :box="box" /> -->
<div class="devtool absolute right-4 text-white bg-black rounded-2xl px-4 py-2">
<!-- <button @click="currentPosition = boxPositions.side">side</button>
<button @click="currentPosition = boxPositions.front">front</button>
<button @click="currentPosition = boxPositions.back">back</button> -->
<div class="w-full block">
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color1" />
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color2" />
<div
class="block w-full h-32"
:style="{
background: `linear-gradient(to top, ${box.color1}, ${box.color2})`
}"
></div>
<!-- <label class="block">
size: {{ size }}
<input v-model.number="size" type="range" step="1" min="1" max="14">
</label> -->
</div>
<!-- <div>
<label class="block">
X: {{ currentPosition.x }}
<input v-model.number="currentPosition.x" type="range" step="1" min="-180" max="180">
</label>
<label class="block">
Y: {{ currentPosition.y }}
<input v-model.number="currentPosition.y" type="range" step="1" min="-180" max="180">
</label>
<label class="block">
Z: {{ currentPosition.z }}
<input v-model.number="currentPosition.z" type="range" step="1" min="-180" max="180">
</label>
</div> -->
</div>
</div>
</div>
<card :track="track" />
</div>
</div>
</template>
<script setup lang="ts">
import type { Box, Track } from '~~/types/types'
const box = ref<Box>({
id: 'ES00A',
name: 'zero',
duration: 2794,
description: 'Zero is for manifesto',
color1: '#ffffff',
color2: '#48959d',
color3: '#00ff00',
type: 'compilation'
})
const track = ref<Track>({
id: 1,
boxId: 'ES00A',
title: 'The grinding wheel',
artist: {
id: 0,
name: "L'Efondras",
url: '',
coverId: '0024705317'
},
start: 0,
url: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
coverId: 'a3236746052',
type: 'compilation'
})
//from-slate-800 to-zinc-900
</script>

View File

@@ -1,19 +1,9 @@
import { defineStore } from 'pinia'
import type { Track } from '~~/types/types'
interface CardPosition {
x: number
y: number
}
type CardPositions = Record<string, Record<number, CardPosition>>
export const useCardStore = defineStore('card', {
state: () => ({
// Stocke les IDs des cartes déjà révélées
revealedCards: new Set<number>(),
// Stocke les positions personnalisées des cartes par box
// Format: { [boxId]: { [trackId]: { x: number, y: number } } }
cardPositions: {} as CardPositions
revealedCards: new Set<number>()
}),
actions: {
@@ -33,20 +23,6 @@ export const useCardStore = defineStore('card', {
return this.revealedCards.has(trackId)
},
// Définir la position d'une carte dans une box
setCardPosition(boxId: string, trackId: number, position: { x: number; y: number }) {
if (!this.cardPositions[boxId]) {
this.cardPositions[boxId] = {}
}
this.cardPositions[boxId][trackId] = position
this.saveToLocalStorage()
},
// Obtenir la position d'une carte dans une box
getCardPosition(boxId: string, trackId: number): { x: number; y: number } | undefined {
return this.cardPositions[boxId]?.[trackId]
},
// Basculer l'état de révélation de toutes les cartes
revealAllCards(tracks: Track[]) {
tracks.forEach((track) => {
@@ -69,8 +45,7 @@ export const useCardStore = defineStore('card', {
localStorage.setItem(
'cardStore',
JSON.stringify({
revealedCards: Array.from(this.revealedCards),
cardPositions: this.cardPositions
revealedCards: Array.from(this.revealedCards)
})
)
} catch (e) {
@@ -85,15 +60,12 @@ export const useCardStore = defineStore('card', {
try {
const saved = localStorage.getItem('cardStore')
if (saved) {
const { revealedCards, cardPositions } = JSON.parse(saved)
const { revealedCards } = JSON.parse(saved)
if (Array.isArray(revealedCards)) {
this.revealedCards = new Set(
revealedCards.filter((id): id is number => typeof id === 'number')
)
}
if (cardPositions && typeof cardPositions === 'object') {
this.cardPositions = cardPositions
}
}
} catch (e) {
console.error('Failed to load card store from localStorage', e)
@@ -111,11 +83,6 @@ export const useCardStore = defineStore('card', {
// Getter pour la réactivité dans les templates
isRevealed: (state) => (trackId: number) => {
return state.revealedCards.has(trackId)
},
// Obtenir toutes les positions des cartes d'une box
getBoxCardPositions: (state) => (boxId: string) => {
return state.cardPositions[boxId] || {}
}
}
})

1
public/card-dock.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="224" xmlns="http://www.w3.org/2000/svg" height="320" id="screenshot-0fc9bf1a-468d-8062-8007-12c4a2492f68" viewBox="0 0 224 320" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0fc9bf1a-468d-8062-8007-12c4a2492f68"><defs><clipPath id="frame-clip-0fc9bf1a-468d-8062-8007-12c4a2492f68-render-1" class="frame-clip frame-clip-def"><rect rx="0" ry="0" x="0" y="0" width="224" height="320" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"/></clipPath></defs><g class="frame-container-wrapper"><g class="frame-container-blur"><g class="frame-container-shadows"><g clip-path="url(#frame-clip-0fc9bf1a-468d-8062-8007-12c4a2492f68-render-1)" fill="none"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c4a2492f68"><rect width="224" height="320" class="frame-background" x="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(255, 255, 255); fill-opacity: 1;" ry="0" rx="0" y="0"/></g><g class="frame-children"><g id="shape-0fc9bf1a-468d-8062-8007-12c9d568a6f9"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c9d568a6f9"><rect rx="20" ry="20" x="0" y="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" width="224" height="320" style="fill: rgb(0, 0, 0); fill-opacity: 0.15;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c8693c73a0"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c8693c73a0"><ellipse cx="112" cy="160.00000000000003" rx="144" ry="144" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c85a924e31"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c85a924e31"><ellipse cx="112" cy="160.00000000000003" rx="112.5" ry="112.50000000000004" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c7cdce5e0a"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c7cdce5e0a"><ellipse cx="112" cy="160" rx="82" ry="82.00000000000003" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c820876e0e"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c820876e0e"><ellipse cx="112" cy="160" rx="51" ry="51" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g></g></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB