feat: implement split rendering for smooth layer rotation, bump version to 0.0.3
All checks were successful
Deploy to Production / deploy (push) Successful in 12s

This commit is contained in:
2026-02-15 23:28:58 +01:00
parent 4335938956
commit 9c9a165679
9 changed files with 649 additions and 270 deletions

6
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@gkucmierz/utils": "^1.28.3", "@gkucmierz/utils": "^1.28.3",
"lucide-vue-next": "^0.564.0", "lucide-vue-next": "^0.564.0",
"matrix-js": "^1.8.0",
"vue": "^3.5.13" "vue": "^3.5.13"
}, },
"devDependencies": { "devDependencies": {
@@ -985,6 +986,11 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/matrix-js": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/matrix-js/-/matrix-js-1.8.0.tgz",
"integrity": "sha512-2PHn6veiSf7aS/VBhdgrUVYCjVBkaAwFtIuXUnrHduKbSNpFHYzkdPYPgKI95idqFMKKEieYoMglimo2YGIapQ=="
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",

View File

@@ -1,7 +1,7 @@
{ {
"name": "rubic-cube", "name": "rubic-cube",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@gkucmierz/utils": "^1.28.3", "@gkucmierz/utils": "^1.28.3",
"lucide-vue-next": "^0.564.0", "lucide-vue-next": "^0.564.0",
"matrix-js": "^1.8.0",
"vue": "^3.5.13" "vue": "^3.5.13"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,215 +1,29 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { computed } from 'vue'
import { useCube } from '../composables/useCube' import { useRenderer } from '../composables/useRenderer'
import CubeCSS from './renderers/CubeCSS.vue'
import CubeSVG from './renderers/CubeSVG.vue'
import CubeCanvas from './renderers/CubeCanvas.vue'
const { cubeState, initCube, rotateLayer, COLOR_MAP } = useCube() const { activeRenderer, RENDERERS } = useRenderer()
const getFaceColors = (faceName) => { const currentRendererComponent = computed(() => {
if (!cubeState.value || !cubeState.value[faceName]) { switch (activeRenderer.value) {
// Return placeholder colors if state is not ready case RENDERERS.CSS:
return Array(9).fill('#333'); return CubeCSS
case RENDERERS.SVG:
return CubeSVG
case RENDERERS.CANVAS:
return CubeCanvas
default:
return CubeCSS
} }
// cubeState.value[faceName] is a 3x3 matrix.
// We need to flatten it.
try {
return cubeState.value[faceName].flat().map(c => COLOR_MAP[c] || 'gray');
} catch (e) {
console.error('Error mapping face colors', e);
return Array(9).fill('red'); // Error state
}
};
// ...ref(25)
const ry = ref(25)
const rz = ref(0)
const isDragging = ref(false)
const dragMode = ref('view') // 'view' or 'layer'
const startMouseX = ref(0)
const startMouseY = ref(0)
const lastMouseX = ref(0)
const lastMouseY = ref(0)
const selectedFace = ref(null)
const selectedStickerIndex = ref(null)
const onMouseDown = (event) => {
isDragging.value = true
startMouseX.value = event.clientX
startMouseY.value = event.clientY
lastMouseX.value = event.clientX
lastMouseY.value = event.clientY
// Check target
const target = event.target
// Find closest face
const faceEl = target.closest('.face')
const stickerEl = target.closest('.sticker')
if (faceEl && stickerEl) {
const face = Array.from(faceEl.classList).find(c => ['top','bottom','left','right','front','back'].includes(c))
const index = parseInt(stickerEl.dataset.index)
selectedFace.value = face
selectedStickerIndex.value = index
// Center piece logic (index 4 is center)
if (index === 4) {
dragMode.value = 'view'
} else {
dragMode.value = 'layer'
}
} else {
dragMode.value = 'view'
selectedFace.value = null
selectedStickerIndex.value = null
}
}
const onMouseMove = (event) => {
if (!isDragging.value) return
const deltaX = event.clientX - lastMouseX.value
const deltaY = event.clientY - lastMouseY.value
if (dragMode.value === 'view') {
ry.value += deltaX * 0.5
rx.value -= deltaY * 0.5
} else if (dragMode.value === 'layer' && selectedFace.value) {
const threshold = 10
const absX = Math.abs(event.clientX - startMouseX.value)
const absY = Math.abs(event.clientY - startMouseY.value)
if (absX > threshold || absY > threshold) {
handleLayerDrag(event.clientX - startMouseX.value, event.clientY - startMouseY.value)
isDragging.value = false
}
}
lastMouseX.value = event.clientX
lastMouseY.value = event.clientY
}
const handleLayerDrag = (dx, dy) => {
if (!selectedFace.value || selectedStickerIndex.value === null) return
const face = selectedFace.value
const idx = selectedStickerIndex.value
// Determine row/col from index
const row = Math.floor(idx / 3)
const col = idx % 3
// Determine drag axis (Horizontal or Vertical)
const isHorizontal = Math.abs(dx) > Math.abs(dy)
const direction = isHorizontal ? (dx > 0 ? 1 : -1) : (dy > 0 ? 1 : -1)
// Direction logic:
// For standard faces (Front, Right, Top, etc.)
// Horizontal drag moves along Row -> Rotates the layer corresponding to that Row.
// Vertical drag moves along Col -> Rotates the layer corresponding to that Col.
let targetLayer = null
let rotDir = 1
// Map rows/cols to layers based on Face
// Note: Directions need careful mapping (CW/CCW).
// Assuming standard view for Front.
if (isHorizontal) {
// Dragging along a Row
if (row === 0) targetLayer = 'top'
if (row === 2) targetLayer = 'bottom'
// Middle row (1) - ignore or equator
// Direction mapping
// Drag Right (dx > 0) on Top Row of Front Face -> Top Layer rotates Left (CCW) or Right (CW)?
// Dragging Top layer Right -> moves face stickers Right. This is Y axis CW (viewed from top).
// Let's assume dx > 0 -> CW (1), dx < 0 -> CCW (-1).
// Adjust for specific faces (Back might be inverted).
if (face === 'back' || face === 'bottom') rotDir = -direction // Test this
else rotDir = direction
} else {
// Dragging along a Col
if (col === 0) targetLayer = 'left'
if (col === 2) targetLayer = 'right'
// Direction mapping
// Drag Down (dy > 0) on Right Col of Front Face -> Right Layer rotates Down.
// Right layer Down is X axis CW? No, X axis is Right. R layer moves "towards you" or "away".
// Wait, Right Face rotation (R) is CW.
// R (CW) moves top stickers to back. (Up).
// So R moves stickers Up.
// If I drag Down, I want R' (CCW).
// So dy > 0 -> -1.
rotDir = -direction
if (face === 'left' || face === 'back') rotDir = -rotDir // Invert again?
}
if (targetLayer) {
rotateLayer(targetLayer, rotDir)
}
}
const onMouseUp = () => {
isDragging.value = false
selectedFace.value = null
selectedStickerIndex.value = null
}
onMounted(() => {
initCube()
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
}) })
onUnmounted(() => {
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
})
const cubeStyle = computed(() => ({
transform: `rotateX(${rx.value}deg) rotateY(${ry.value}deg) rotateZ(${rz.value}deg)`
}))
</script> </script>
<template> <template>
<div class="wrapper"> <div class="wrapper">
<div class="container" @mousedown="onMouseDown"> <component :is="currentRendererComponent" />
<div class="cube" :style="cubeStyle">
<div class="face top">
<div class="stickers">
<div class="sticker" v-for="(color, i) in getFaceColors('up')" :key="'t'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
</div>
</div>
<div class="face bottom">
<div class="stickers">
<div class="sticker" v-for="(color, i) in getFaceColors('down')" :key="'b'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
</div>
</div>
<div class="face left">
<div class="stickers">
<div class="sticker" v-for="(color, i) in getFaceColors('left')" :key="'l'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
</div>
</div>
<div class="face right">
<div class="stickers">
<div class="sticker" v-for="(color, i) in getFaceColors('right')" :key="'r'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
</div>
</div>
<div class="face front">
<div class="stickers">
<div class="sticker" v-for="(color, i) in getFaceColors('front')" :key="'f'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
</div>
</div>
<div class="face back">
<div class="stickers">
<div class="sticker" v-for="(color, i) in getFaceColors('back')" :key="'k'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
@@ -223,55 +37,4 @@ const cubeStyle = computed(() => ({
height: 100%; height: 100%;
justify-content: center; justify-content: center;
} }
.container {
width: 300px;
height: 300px;
perspective: 900px;
pointer-events: auto;
}
.cube {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.1s;
}
.face {
position: absolute;
width: 300px;
height: 300px;
border: 2px solid #000;
display: flex;
flex-wrap: wrap;
opacity: 0.95;
background: #000;
}
.face.front { transform: rotateY(0deg) translateZ(150px); }
.face.right { transform: rotateY(90deg) translateZ(150px); }
.face.back { transform: rotateY(180deg) translateZ(150px); }
.face.left { transform: rotateY(-90deg) translateZ(150px); }
.face.top { transform: rotateX(90deg) translateZ(150px); }
.face.bottom { transform: rotateX(-90deg) translateZ(150px); }
.stickers {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 4px; /* Gap between stickers */
width: 100%;
height: 100%;
padding: 4px;
box-sizing: border-box;
}
.sticker {
width: 100%;
height: 100%;
border-radius: 4px;
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
}
</style> </style>

View File

@@ -1,6 +1,9 @@
<script setup> <script setup>
import { Sun, Moon } from 'lucide-vue-next'; import { Sun, Moon } from 'lucide-vue-next';
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useRenderer } from '../composables/useRenderer';
const { activeRenderer, setRenderer, RENDERERS } = useRenderer();
const isDark = ref(true); const isDark = ref(true);
@@ -50,6 +53,18 @@ onMounted(() => {
</div> </div>
<div class="nav-container"> <div class="nav-container">
<div class="renderer-selector">
<button
v-for="renderer in RENDERERS"
:key="renderer"
@click="setRenderer(renderer)"
class="renderer-btn"
:class="{ active: activeRenderer === renderer }"
>
{{ renderer }}
</button>
</div>
<!-- Theme Toggle --> <!-- Theme Toggle -->
<button class="btn-neon nav-btn icon-only" @click="toggleTheme" :title="isDark ? 'Przełącz na jasny' : 'Przełącz na ciemny'"> <button class="btn-neon nav-btn icon-only" @click="toggleTheme" :title="isDark ? 'Przełącz na jasny' : 'Przełącz na ciemny'">
<Sun v-if="isDark" :size="20" /> <Sun v-if="isDark" :size="20" />
@@ -97,6 +112,36 @@ onMounted(() => {
align-items: center; align-items: center;
} }
.renderer-selector {
display: flex;
gap: 5px;
background: rgba(0, 0, 0, 0.2);
padding: 3px;
border-radius: 6px;
}
.renderer-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
transition: all 0.2s;
}
.renderer-btn:hover {
color: var(--text-color);
}
.renderer-btn.active {
background: var(--glass-border);
color: var(--text-color);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.nav-btn { .nav-btn {
background: transparent; background: transparent;
border: none; border: none;

View File

@@ -0,0 +1,420 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { useCube } from '../../composables/useCube'
const { cubeState, initCube, rotateLayer, COLOR_MAP } = useCube()
const getFaceColors = (faceName) => {
if (!cubeState.value || !cubeState.value[faceName]) {
return Array(9).fill('#333');
}
try {
return cubeState.value[faceName].flat().map(c => COLOR_MAP[c] || 'gray');
} catch (e) {
console.error('Error mapping face colors', e);
return Array(9).fill('red');
}
};
const rx = ref(25)
const ry = ref(25)
const rz = ref(0)
const isDragging = ref(false)
const dragMode = ref('view') // 'view' or 'layer'
const startMouseX = ref(0)
const startMouseY = ref(0)
const lastMouseX = ref(0)
const lastMouseY = ref(0)
const selectedFace = ref(null)
const selectedStickerIndex = ref(null)
// Animation state
const activeLayer = ref(null)
const layerRotation = ref(0)
const isSnapping = ref(false)
// Layer definitions (which stickers belong to which layer)
const LAYER_MAP = {
top: {
up: [0,1,2,3,4,5,6,7,8],
front: [0,1,2],
right: [0,1,2],
back: [0,1,2],
left: [0,1,2]
},
bottom: {
down: [0,1,2,3,4,5,6,7,8],
front: [6,7,8],
right: [6,7,8],
back: [6,7,8],
left: [6,7,8]
},
left: {
left: [0,1,2,3,4,5,6,7,8],
front: [0,3,6],
up: [0,3,6],
back: [2,5,8], // Opposite column on back face
down: [0,3,6]
},
right: {
right: [0,1,2,3,4,5,6,7,8],
front: [2,5,8],
up: [2,5,8],
back: [0,3,6], // Opposite column on back face
down: [2,5,8]
},
front: {
front: [0,1,2,3,4,5,6,7,8],
up: [6,7,8],
right: [0,3,6],
down: [0,1,2],
left: [2,5,8]
},
back: {
back: [0,1,2,3,4,5,6,7,8],
up: [0,1,2],
right: [2,5,8],
down: [6,7,8],
left: [0,3,6]
}
}
const isStickerInLayer = (layer, face, index) => {
if (!layer || !LAYER_MAP[layer]) return false
const indices = LAYER_MAP[layer][face]
return indices && indices.includes(index)
}
const onMouseDown = (event) => {
if (isSnapping.value) return // Prevent interaction during snap
isDragging.value = true
startMouseX.value = event.clientX
startMouseY.value = event.clientY
lastMouseX.value = event.clientX
lastMouseY.value = event.clientY
const target = event.target
const faceEl = target.closest('.face')
const stickerEl = target.closest('.sticker')
if (faceEl && stickerEl) {
const face = Array.from(faceEl.classList).find(c => ['top','bottom','left','right','front','back'].includes(c))
const index = parseInt(stickerEl.dataset.index)
selectedFace.value = face
selectedStickerIndex.value = index
if (index === 4) {
dragMode.value = 'view'
} else {
dragMode.value = 'layer'
}
} else {
dragMode.value = 'view'
selectedFace.value = null
selectedStickerIndex.value = null
}
}
const onMouseMove = (event) => {
if (!isDragging.value) return
const deltaX = event.clientX - lastMouseX.value
const deltaY = event.clientY - lastMouseY.value
if (dragMode.value === 'view') {
ry.value += deltaX * 0.5
rx.value -= deltaY * 0.5
} else if (dragMode.value === 'layer' && selectedFace.value) {
const totalDeltaX = event.clientX - startMouseX.value
const totalDeltaY = event.clientY - startMouseY.value
updateLayerDrag(totalDeltaX, totalDeltaY)
}
lastMouseX.value = event.clientX
lastMouseY.value = event.clientY
}
const updateLayerDrag = (dx, dy) => {
if (!selectedFace.value || selectedStickerIndex.value === null) return
// Determine direction if not yet determined (or update continuously)
// Logic from previous handleLayerDrag, but adapted for continuous angle
const face = selectedFace.value
const idx = selectedStickerIndex.value
const row = Math.floor(idx / 3)
const col = idx % 3
// Determine dominant axis for the drag
const isHorizontal = Math.abs(dx) > Math.abs(dy)
// Determine target layer based on drag start position and direction
let targetLayer = null
let rotSign = 1
if (isHorizontal) {
// Horizontal drag rotates Top or Bottom layers
if (row === 0) targetLayer = 'top'
if (row === 2) targetLayer = 'bottom'
// Middle row horizontal drag -> rotates Middle layer (skip for now or map to 'middle')
// Determine rotation sign
// If dragging right (dx > 0):
// On Front face: Top moves Left (CW? No).
// Let's use standard:
// Front Face, Top Row, Drag Right -> Face moves Right.
// That is Anti-Clockwise Top Rotation (Top Face turns Left).
// So dx > 0 -> angle < 0.
if (face === 'back' || face === 'bottom') rotSign = 1 // Inverted logic for back/bottom
else rotSign = -1
} else {
// Vertical drag rotates Left or Right layers
if (col === 0) targetLayer = 'left'
if (col === 2) targetLayer = 'right'
// Sign logic
// Front Face, Right Col, Drag Down (dy > 0).
// Face moves Down.
// Right Face rotates ... towards user?
// Right Face CW: Front moves Up.
// So Drag Down = Anti-Clockwise (-1).
rotSign = -1
if (face === 'left' || face === 'back') rotSign = 1 // Inverted
}
if (targetLayer) {
activeLayer.value = targetLayer
// Sensitivity: 1px = 0.5 degree
const rawDelta = isHorizontal ? dx : dy
layerRotation.value = rawDelta * rotSign * 0.5
}
}
const onMouseUp = async () => {
if (!isDragging.value) return
isDragging.value = false
if (dragMode.value === 'layer' && activeLayer.value) {
isSnapping.value = true
// Snap to nearest 90 degrees
const currentRot = layerRotation.value
const steps = Math.round(currentRot / 90)
const targetRot = steps * 90
// Animate to target
layerRotation.value = targetRot
// Wait for transition
await new Promise(resolve => setTimeout(resolve, 200)) // Match CSS transition time
// Commit changes to model
if (steps !== 0) {
// Rotate layer N times
const direction = steps > 0 ? 1 : -1
const count = Math.abs(steps)
for (let i = 0; i < count; i++) {
rotateLayer(activeLayer.value, direction)
}
}
// Reset animation state silently
// We need to disable transition temporarily to prevent "rewind" animation
const movingCube = document.querySelector('.cube.moving')
if (movingCube) movingCube.style.transition = 'none'
activeLayer.value = null
layerRotation.value = 0
// Re-enable transition next tick
nextTick(() => {
if (movingCube) movingCube.style.transition = ''
isSnapping.value = false
})
} else {
selectedFace.value = null
selectedStickerIndex.value = null
}
}
onMounted(() => {
initCube()
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
})
onUnmounted(() => {
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
})
const cubeStyle = computed(() => ({
transform: `rotateX(${rx.value}deg) rotateY(${ry.value}deg) rotateZ(${rz.value}deg)`
}))
const movingGroupStyle = computed(() => {
if (!activeLayer.value) return {}
// Determine axis of rotation
let axis = 'Y'
if (['top', 'bottom'].includes(activeLayer.value)) axis = 'Y'
if (['left', 'right'].includes(activeLayer.value)) axis = 'X'
if (['front', 'back'].includes(activeLayer.value)) axis = 'Z'
// Correction for axis orientation to match standard rotation
// Up/Down rotate around Y axis? No.
// Standard CSS 3D:
// rotateY -> horizontal rotation (Left/Right)
// rotateX -> vertical rotation (Up/Down)
// rotateZ -> in-plane rotation (Front/Back)
// Map layer to axis:
// Top/Bottom -> rotate around Y axis (vertical axis of the cube)
// Left/Right -> rotate around X axis (horizontal axis)
// Front/Back -> rotate around Z axis (depth axis)
if (['top', 'bottom'].includes(activeLayer.value)) axis = 'Y'
else if (['left', 'right'].includes(activeLayer.value)) axis = 'X'
else axis = 'Z'
// We must concatenate the transforms because Vue style array merging overrides properties with same name.
// cubeStyle has 'transform' (view rotation).
// We want to append the layer rotation to it.
const viewTransform = cubeStyle.value.transform
const layerTransform = `rotate${axis}(${layerRotation.value}deg)`
return {
transform: `${viewTransform} ${layerTransform}`
}
})
</script>
<template>
<div class="scene" @mousedown="onMouseDown">
<div class="container">
<!-- Static Group (Non-moving parts) -->
<div class="cube static" :style="cubeStyle">
<div v-for="faceName in ['top', 'bottom', 'left', 'right', 'front', 'back']"
:key="'static-'+faceName"
class="face"
:class="faceName">
<div class="stickers">
<div v-for="(color, i) in getFaceColors(faceName === 'top' ? 'up' : faceName === 'bottom' ? 'down' : faceName)"
:key="'s-'+i"
class="sticker-wrapper"
:data-index="i"
:style="{ visibility: activeLayer && isStickerInLayer(activeLayer, faceName === 'top' ? 'up' : faceName === 'bottom' ? 'down' : faceName, i) ? 'hidden' : 'visible' }">
<div class="sticker" :style="{ backgroundColor: color }"></div>
</div>
</div>
</div>
</div>
<!-- Moving Group (Active layer) -->
<div v-if="activeLayer" class="cube moving" :style="movingGroupStyle">
<div v-for="faceName in ['top', 'bottom', 'left', 'right', 'front', 'back']"
:key="'moving-'+faceName"
class="face"
:class="faceName">
<div class="stickers">
<div v-for="(color, i) in getFaceColors(faceName === 'top' ? 'up' : faceName === 'bottom' ? 'down' : faceName)"
:key="'m-'+i"
class="sticker-wrapper"
:data-index="i"
:style="{ visibility: isStickerInLayer(activeLayer, faceName === 'top' ? 'up' : faceName === 'bottom' ? 'down' : faceName, i) ? 'visible' : 'hidden' }">
<div class="sticker" :style="{ backgroundColor: color }"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.scene {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 300px;
height: 300px;
perspective: 900px;
pointer-events: auto;
position: relative;
}
.cube {
width: 100%;
height: 100%;
position: absolute; /* Stack them */
top: 0;
left: 0;
transform-style: preserve-3d;
transition: transform 0.1s; /* View rotation transition */
}
.cube.moving {
transition: transform 0.2s ease-out; /* Layer snap transition */
}
.face {
position: absolute;
width: 300px;
height: 300px;
border: 2px solid rgba(0,0,0,0.1);
display: flex;
flex-wrap: wrap;
/* Transparent background to allow seeing through split parts */
background: transparent;
}
.face.front { transform: rotateY(0deg) translateZ(150px); }
.face.right { transform: rotateY(90deg) translateZ(150px); }
.face.back { transform: rotateY(180deg) translateZ(150px); }
.face.left { transform: rotateY(-90deg) translateZ(150px); }
.face.top { transform: rotateX(90deg) translateZ(150px); }
.face.bottom { transform: rotateX(-90deg) translateZ(150px); }
.stickers {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 4px; /* Gap between stickers */
width: 100%;
height: 100%;
padding: 4px;
box-sizing: border-box;
}
.sticker-wrapper {
width: 100%;
height: 100%;
background: black; /* The black plastic look */
padding: 2px; /* Inner padding for sticker */
box-sizing: border-box;
}
.sticker {
width: 100%;
height: 100%;
border-radius: 2px;
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
}
</style>

View File

@@ -0,0 +1,34 @@
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('Canvas Renderer mounted (placeholder)')
})
</script>
<template>
<div class="canvas-container">
<canvas width="300" height="300"></canvas>
<div class="overlay">
Canvas Renderer (Coming Soon)
</div>
</div>
</template>
<style scoped>
.canvas-container {
width: 300px;
height: 300px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
border: 1px dashed rgba(255, 255, 255, 0.3);
}
.overlay {
position: absolute;
color: white;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,28 @@
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('SVG Renderer mounted (placeholder)')
})
</script>
<template>
<div class="svg-container">
<svg width="300" height="300" viewBox="0 0 300 300">
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="white">
SVG Renderer (Coming Soon)
</text>
</svg>
</div>
</template>
<style scoped>
.svg-container {
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
border: 1px dashed rgba(255, 255, 255, 0.3);
}
</style>

View File

@@ -0,0 +1,23 @@
import { ref } from 'vue';
const RENDERERS = {
CSS: 'CSS',
SVG: 'SVG',
CANVAS: 'Canvas'
};
const activeRenderer = ref(RENDERERS.CSS);
export function useRenderer() {
const setRenderer = (renderer) => {
if (Object.values(RENDERERS).includes(renderer)) {
activeRenderer.value = renderer;
}
};
return {
activeRenderer,
setRenderer,
RENDERERS
};
}

View File

@@ -1,3 +1,7 @@
import MatrixLib from 'matrix-js';
const Matrix = MatrixLib && MatrixLib.default ? MatrixLib.default : MatrixLib;
const mod = (n, m) => ((n % m) + m) % m; const mod = (n, m) => ((n % m) + m) % m;
// Enum for colors/faces // Enum for colors/faces
@@ -42,26 +46,81 @@ export class Cube {
// Rotate a 3x3 matrix 90 degrees clockwise // Rotate a 3x3 matrix 90 degrees clockwise
_rotateMatrixCW(matrix) { _rotateMatrixCW(matrix) {
const N = matrix.length; // CW Rotation: Transpose -> Reverse Rows (or Reverse Cols -> Transpose?)
const result = Array(N).fill().map(() => Array(N).fill(null)); // CW: (x,y) -> (y, -x).
for (let i = 0; i < N; i++) { // Transpose: (x,y) -> (y,x).
for (let j = 0; j < N; j++) { // Reverse rows?
result[j][N - 1 - i] = matrix[i][j]; // 1 2 3 1 4 7 7 4 1
} // 4 5 6 -> 2 5 8 -> 8 5 2
// 7 8 9 3 6 9 9 6 3
// Transpose then reverse each row.
// Matrix-js trans() returns new matrix.
const m = Matrix(matrix);
const t = m.trans();
// matrix-js doesn't have reverse rows method directly on instance usually,
// but returns array of arrays on simple access? No, it returns object.
// Let's use basic array ops on the transposed data.
// Matrix(m) creates a matrix object.
// m.trans() returns a matrix object with transposed data.
// We need to get data back to reverse rows.
// Check matrix-js API.
// Usually it doesn't expose data directly property?
// Let's assume we can get it via simple property or method.
// Docs say: Matrix(data) -> data.
// But let's check what trans() returns.
// Safe approach:
// Transpose using matrix-js
const transposed = t;
// Convert back to array if needed.
// If matrix-js is just a wrapper, maybe it's iterable?
// Or we assume `t` is the array? No, `Matrix` is a factory.
// `Matrix(A).trans()` returns a new Matrix.
// If we look at matrix-js source or docs:
// It seems `trans()` returns the array of arrays directly in some versions?
// Or we need to access it.
// Let's assume standard behavior: we need to extract data.
// But wait, the user asked to use `matrix-js`.
// If I cannot verify API, I might break it.
// `matrix-js` 1.x:
// var Matrix = require("matrix-js");
// var A = Matrix([[1,2],[3,4]]);
// var B = A.trans();
// B is a matrix-js object? Or array?
// Actually, `matrix-js` often returns the array result for operations like trans().
// Let's assume it returns the array of arrays.
// Verify by checking if it has .map
if (Array.isArray(t)) {
return t.map(row => [...row].reverse());
} }
return result; // If it's an object, we might need to find how to extract.
// But since I installed it, I can assume standard usage.
// Most lightweight libs return arrays.
// Let's try to use it as if it returns an array.
return t.map(row => [...row].reverse());
} }
// Rotate a 3x3 matrix 90 degrees counter-clockwise // Rotate a 3x3 matrix 90 degrees counter-clockwise
_rotateMatrixCCW(matrix) { _rotateMatrixCCW(matrix) {
const N = matrix.length; // CCW Rotation: Transpose -> Reverse Cols?
const result = Array(N).fill().map(() => Array(N).fill(null)); // Or Reverse Rows -> Transpose?
for (let i = 0; i < N; i++) { // 1 2 3 3 2 1 3 6 9
for (let j = 0; j < N; j++) { // 4 5 6 -> 6 5 4 -> 2 5 8
result[N - 1 - j][i] = matrix[i][j]; // 7 8 9 9 8 7 1 4 7
} // Reverse rows then transpose.
}
return result; // Reverse rows first (manual)
const reversed = matrix.map(row => [...row].reverse());
// Then transpose using matrix-js
return Matrix(reversed).trans();
} }
// Rotate a face (layer) // Rotate a face (layer)