diff --git a/package.json b/package.json index 33ae955..b7567ef 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rubic-cube", "private": true, - "version": "0.0.3", + "version": "0.0.4", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/renderers/CubeCSS.vue b/src/components/renderers/CubeCSS.vue index 6bdefee..d410879 100644 --- a/src/components/renderers/CubeCSS.vue +++ b/src/components/renderers/CubeCSS.vue @@ -2,20 +2,9 @@ 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 { cubeState, initCube, rotateLayer, COLOR_MAP, FACES } = useCube() +// --- State --- const rx = ref(25) const ry = ref(25) const rz = ref(0) @@ -26,227 +15,439 @@ 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 selectedCubieId = ref(null) // ID of the cubie where drag started +const selectedFaceNormal = ref(null) // Normal vector of the face clicked // Animation state -const activeLayer = ref(null) -const layerRotation = ref(0) -const isSnapping = ref(false) + const activeLayer = ref(null) // { axis: 'x'|'y'|'z', index: -1|0|1 } + const layerRotation = ref(0) + const isSnapping = ref(false) + const velocity = ref(0) + const lastTime = ref(0) + const rafId = ref(null) -// 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] +// Cubies Model +// We represent the cube as 27 independent cubies. +// Each cubie has a current position (x, y, z) in grid coordinates [-1, 0, 1]. +// And a rotation matrix (or simplified orientation). +// Actually, for CSS rendering, we can just keep track of their current (x,y,z) and applying transforms. +// But to match `useCube` state (which is color based), we need to map colors to cubies. +// +// Alternative: +// `useCube` maintains the logical state of colors on faces. +// To render 27 cubies that MOVE, we need to know which color belongs to which face of which cubie. +// +// Mapping: +// 27 Cubies. ID: 0..26. +// Position: x,y,z in {-1, 0, 1}. +// +// Colors: +// A cubie at (x,y,z) exposes faces if x/y/z is +/- 1. +// e.g. (1, 1, 1) is Right-Top-Front corner. +// It has 3 colored faces: Right, Top, Front. +// We need to fetch the color from `cubeState` at the correct indices. +// +// `cubeState` is organized by Faces. +// Front Face is a 3x3 matrix. +// (0,0) is Top-Left of Front Face. +// Front Face covers z=1 plane. +// x goes -1 (Left) to 1 (Right). +// y goes 1 (Top) to -1 (Bottom). +// +// Let's define the 27 cubies. +const cubies = ref([]) + +const initCubies = () => { + const newCubies = [] + let id = 0 + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + for (let z = -1; z <= 1; z++) { + newCubies.push({ + id: id++, + x, y, z, // Current grid position + // Store initial rotation or accumulate transform? + // Simplest is to accumulate rotation transforms for the cubie div. + // But for logic, we update x,y,z after snap. + transform: '' + }) + } + } } + cubies.value = newCubies } -const isStickerInLayer = (layer, face, index) => { - if (!layer || !LAYER_MAP[layer]) return false - const indices = LAYER_MAP[layer][face] - return indices && indices.includes(index) +// Map logical face colors to cubie faces +// We need a function that given a cubie (x,y,z) returns the colors of its 6 faces. +// If a face is internal, color is black (or null). +const getCubieFaces = (cubie) => { + const { x, y, z } = cubie + const faces = {} + + // Helper to map grid (x,y) to Matrix indices (row, col) + // Grid: x (-1..1), y (-1..1). + // Matrix: row (0..2), col (0..2). + // + // Face UP (y=1). z: Back(-1)..Front(1). x: Left(-1)..Right(1). + // Up Matrix: row 0 is Back, row 2 is Front. col 0 is Left, col 2 is Right. + // So: row = z + 1? No. + // z=-1 -> row 0. z=0 -> row 1. z=1 -> row 2. Yes. + // x=-1 -> col 0. x=0 -> col 1. x=1 -> col 2. Yes. + if (y === 1) { + const row = z + 1 + const col = x + 1 + faces.up = getColor(FACES.UP, row, col) + } + + // Face DOWN (y=-1). z: Back(-1)..Front(1). x: Left(-1)..Right(1). + // Down Matrix: row 0 is Front, row 2 is Back. col 0 is Left, col 2 is Right. + // Wait, check standard mapping in `Cube.js` or standard rubik. + // Usually unfolding: + // Up: Back row is top. + // Down: Front row is top? + // Let's assume standard intuitive mapping: + // Down Face viewed from bottom. + // Row 0 is Front (top of view). + // z=1 -> row 0. z=-1 -> row 2. + // So row = 1 - z. + // x=-1 -> col 0. x=1 -> col 2. + if (y === -1) { + const row = 1 - z + const col = x + 1 + faces.down = getColor(FACES.DOWN, row, col) + } + + // Face FRONT (z=1). y: Top(1)..Bottom(-1). x: Left(-1)..Right(1). + // Matrix: row 0 is Top. + // y=1 -> row 0. y=-1 -> row 2. + // row = 1 - y. + // x=-1 -> col 0. + if (z === 1) { + const row = 1 - y + const col = x + 1 + faces.front = getColor(FACES.FRONT, row, col) + } + + // Face BACK (z=-1). y: Top(1)..Bottom(-1). x: Right(1)..Left(-1)? + // Back Face viewed from Back. + // Left side of view is Cube Right (x=1). + // Right side of view is Cube Left (x=-1). + // Matrix: row 0 is Top. + // y=1 -> row 0. + // col 0 (Left of view) -> x=1. + // col 2 (Right of view) -> x=-1. + // col = 1 - x. + if (z === -1) { + const row = 1 - y + const col = 1 - x + faces.back = getColor(FACES.BACK, row, col) + } + + // Face RIGHT (x=1). y: Top(1)..Bottom(-1). z: Front(1)..Back(-1). + // Right Face viewed from Right. + // Left side of view is Front (z=1). + // Right side of view is Back (z=-1). + // Matrix: row 0 is Top. + // y=1 -> row 0. + // col 0 -> z=1. + // col 2 -> z=-1. + // col = 1 - z. + if (x === 1) { + const row = 1 - y + const col = 1 - z + faces.right = getColor(FACES.RIGHT, row, col) + } + + // Face LEFT (x=-1). y: Top(1)..Bottom(-1). z: Back(-1)..Front(1). + // Left Face viewed from Left. + // Left side of view is Back (z=-1). + // Right side of view is Front (z=1). + // Matrix: row 0 is Top. + // y=1 -> row 0. + // col 0 -> z=-1. + // col 2 -> z=1. + // col = z + 1. + if (x === -1) { + const row = 1 - y + const col = z + 1 + faces.left = getColor(FACES.LEFT, row, col) + } + + return faces } +const getColor = (face, row, col) => { + if (!cubeState.value || !cubeState.value[face]) return 'black' + const colorIndex = cubeState.value[face][row][col] + return COLOR_MAP[colorIndex] || 'black' +} + +// Mouse Interaction const onMouseDown = (event) => { - if (isSnapping.value) return // Prevent interaction during snap + if (isSnapping.value) return isDragging.value = true startMouseX.value = event.clientX startMouseY.value = event.clientY lastMouseX.value = event.clientX lastMouseY.value = event.clientY + lastTime.value = performance.now() + velocity.value = 0 // Reset velocity const target = event.target - const faceEl = target.closest('.face') - const stickerEl = target.closest('.sticker') + const stickerEl = target.closest('.sticker-face') - 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) + if (stickerEl) { + // Clicked on a cubie face + const cubieId = parseInt(stickerEl.dataset.cubieId) + const faceName = stickerEl.dataset.face - selectedFace.value = face - selectedStickerIndex.value = index - - if (index === 4) { + selectedCubieId.value = cubieId + selectedFaceNormal.value = faceName // 'up', 'down', etc. + + // Check if it's a center face (implies View Drag)? + // User wants drag to rotate layers if grabbing edge/corner. + // Center face of the whole cube? No, center face of a side. + // If I grab the center sticker of Front Face, I might want to rotate View OR Front Face? + // User said: "jedynie centralny element kostki dragowany, bedzie ja po prostu obracal" + // So Center Sticker -> View Drag. + // Center Sticker is when x,y,z has two zeros? No. + // Center of Front Face: (0,0,1). + // Edge: (1,0,1). Corner: (1,1,1). + + const cubie = cubies.value.find(c => c.id === cubieId) + const isCenter = (Math.abs(cubie.x) + Math.abs(cubie.y) + Math.abs(cubie.z)) === 1 + + if (isCenter) { dragMode.value = 'view' + document.body.style.cursor = 'move' } else { dragMode.value = 'layer' + document.body.style.cursor = 'grab' } } else { dragMode.value = 'view' - selectedFace.value = null - selectedStickerIndex.value = null + selectedCubieId.value = null + document.body.style.cursor = 'move' } } const onMouseMove = (event) => { if (!isDragging.value) return + if (dragMode.value === 'layer') { + document.body.style.cursor = 'grabbing' + } + 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) { + velocity.value = 0 // Reset velocity for view drag (or track it separately if needed) + } else if (dragMode.value === 'layer' && selectedCubieId.value !== null) { const totalDeltaX = event.clientX - startMouseX.value const totalDeltaY = event.clientY - startMouseY.value - updateLayerDrag(totalDeltaX, totalDeltaY) + // Calculate velocity + const now = performance.now() + const dt = now - lastTime.value + lastTime.value = now + + // We only care about velocity of rotation, so we calculate it inside updateLayerDrag? + // Or we track mouse velocity here. + // Let's track rotation velocity in updateLayerDrag to be accurate with axis mapping. + updateLayerDrag(totalDeltaX, totalDeltaY, dt) } lastMouseX.value = event.clientX lastMouseY.value = event.clientY } -const updateLayerDrag = (dx, dy) => { - if (!selectedFace.value || selectedStickerIndex.value === null) return +const updateLayerDrag = (dx, dy, dt) => { + // Determine rotation axis and direction based on drag vector and clicked face + const cubie = cubies.value.find(c => c.id === selectedCubieId.value) + if (!cubie) return + + // Need to map 2D drag to 3D axis. + // Face Normals: + // Front: Z. Right: X. Up: Y. - // Determine direction if not yet determined (or update continuously) - // Logic from previous handleLayerDrag, but adapted for continuous angle + const face = selectedFaceNormal.value + let axis = null + let sign = 1 - const face = selectedFace.value - const idx = selectedStickerIndex.value + const absDx = Math.abs(dx) + const absDy = Math.abs(dy) + const isHorizontal = absDx > absDy - 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 + // Logic: + if (face === 'front' || face === 'back') { + if (isHorizontal) axis = 'y'; else axis = 'x'; + } else if (face === 'right' || face === 'left') { + if (isHorizontal) axis = 'y'; else axis = 'z'; + } else if (face === 'up' || face === 'down') { + if (isHorizontal) axis = 'y'; + else axis = 'x'; } - if (targetLayer) { - activeLayer.value = targetLayer - // Sensitivity: 1px = 0.5 degree - const rawDelta = isHorizontal ? dx : dy - layerRotation.value = rawDelta * rotSign * 0.5 + if (!axis) return + + // Determine layer index + let index = 0 + if (axis === 'x') index = cubie.x + if (axis === 'y') index = cubie.y + if (axis === 'z') index = cubie.z + + activeLayer.value = { axis, index } + + // Determine Sign (Visual mapping) + const delta = isHorizontal ? dx : dy + const baseSign = isHorizontal ? 1 : -1 + + const newRotation = delta * baseSign * 0.5 + + // Calculate velocity (deg/ms) + if (dt > 0) { + const dRot = newRotation - layerRotation.value + // Simple low-pass filter for smoothing + velocity.value = 0.6 * velocity.value + 0.4 * (dRot / dt) } + + layerRotation.value = newRotation } const onMouseUp = async () => { if (!isDragging.value) return isDragging.value = false + document.body.style.cursor = '' if (dragMode.value === 'layer' && activeLayer.value) { isSnapping.value = true + // Inertia calculation + // Project final position based on velocity + // 200ms projection is reasonable for "throw" feel + const projection = velocity.value * 200 + const projectedRot = layerRotation.value + projection + // Snap to nearest 90 degrees - const currentRot = layerRotation.value - const steps = Math.round(currentRot / 90) + const steps = Math.round(projectedRot / 90) const targetRot = steps * 90 - // Animate to target - layerRotation.value = targetRot + // Animation Loop + const startRot = layerRotation.value + const startTime = performance.now() + const duration = 300 // ms - // Wait for transition - await new Promise(resolve => setTimeout(resolve, 200)) // Match CSS transition time + // Ease out cubic function + const easeOut = (t) => 1 - Math.pow(1 - t, 3) - // 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) + return new Promise(resolve => { + const animate = (time) => { + const elapsed = time - startTime + const progress = Math.min(elapsed / duration, 1) + const ease = easeOut(progress) + + layerRotation.value = startRot + (targetRot - startRot) * ease + + if (progress < 1) { + rafId.value = requestAnimationFrame(animate) + } else { + finishRotation(steps) + resolve() + } } - } - - // 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 + rafId.value = requestAnimationFrame(animate) }) - } else { - selectedFace.value = null - selectedStickerIndex.value = null + selectedCubieId.value = null } } +const finishRotation = (steps) => { + if (steps !== 0) { + // Update logical state + const { axis, index } = activeLayer.value + let layerName = null + + if (axis === 'x') { + if (index === -1) layerName = 'left' + if (index === 1) layerName = 'right' + } else if (axis === 'y') { + if (index === 1) layerName = 'top' + if (index === -1) layerName = 'bottom' + } else if (axis === 'z') { + if (index === 1) layerName = 'front' + if (index === -1) layerName = 'back' + } + + if (layerName) { + // Apply rotation to logical cube + let direction = steps > 0 ? 1 : -1 + + // Invert direction for specific layers where Visual and Logical rotations are opposite + if (layerName === 'top' || layerName === 'back' || layerName === 'right') { + direction = -direction + } + + const count = Math.abs(steps) + + // 1. Update Logical State (Colors) + for (let i = 0; i < count; i++) { + rotateLayer(layerName, direction) + } + + // 2. Update Visual State (Cubies Position) + // We must rotate the (x,y,z) coordinates of the cubies that were in the active layer. + const visualSteps = steps // + means +90deg along axis + + // Apply N times + const rotations = Math.abs(visualSteps) + const sign = Math.sign(visualSteps) + + for (let r = 0; r < rotations; r++) { + cubies.value.forEach(cubie => { + // Check if cubie is in the rotating layer + let inLayer = false + if (axis === 'x' && cubie.x === index) inLayer = true + if (axis === 'y' && cubie.y === index) inLayer = true + if (axis === 'z' && cubie.z === index) inLayer = true + + if (inLayer) { + const { x, y, z } = cubie + let nx = x, ny = y, nz = z + + if (axis === 'x') { + if (sign > 0) { ny = -z; nz = y; } // (x, -z, y) + else { ny = z; nz = -y; } // (x, z, -y) + } else if (axis === 'y') { + if (sign > 0) { nx = z; nz = -x; } // (z, y, -x) + else { nx = -z; nz = x; } // (-z, y, x) + } else if (axis === 'z') { + if (sign > 0) { nx = -y; ny = x; } // (-y, x, z) + else { nx = y; ny = -x; } // (y, -x, z) + } + + cubie.x = nx + cubie.y = ny + cubie.z = nz + } + }) + } + } + } + + activeLayer.value = null + layerRotation.value = 0 + isSnapping.value = false + velocity.value = 0 +} + +// Lifecycle onMounted(() => { + initCubies() initCube() window.addEventListener('mousemove', onMouseMove) window.addEventListener('mouseup', onMouseUp) @@ -255,89 +456,79 @@ onMounted(() => { onUnmounted(() => { window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mouseup', onMouseUp) + if (rafId.value) cancelAnimationFrame(rafId.value) }) +// Styles const cubeStyle = computed(() => ({ transform: `rotateX(${rx.value}deg) rotateY(${ry.value}deg) rotateZ(${rz.value}deg)` })) -const movingGroupStyle = computed(() => { - if (!activeLayer.value) return {} +const getCubieStyle = (cubie) => { + // Base position + // scale 300px total. 100px per cubie. + // x,y,z in -1..1. + // translate(x*100, -y*100, z*100). + // Y is inverted in CSS (down is positive)? + // Usually in 3D CSS: + // X right, Y down, Z towards viewer. + // My Grid: Y=1 is Top. + // So Y=1 -> translateY(-100px). - // 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' + const tx = cubie.x * 100 + const ty = cubie.y * -100 + const tz = cubie.z * 100 - // 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) + let transform = `translate3d(${tx}px, ${ty}px, ${tz}px)` - // 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}` + // Apply rotation if active layer + if (activeLayer.value) { + const { axis, index } = activeLayer.value + let match = false + if (axis === 'x' && cubie.x === index) match = true + if (axis === 'y' && cubie.y === index) match = true + if (axis === 'z' && cubie.z === index) match = true + + if (match) { + // Rotation origin is center of cube (0,0,0). + // But we are translating the cubie. + // To rotate around global axis, we should rotate THEN translate? + // No, the Group rotates. + // But here we rotate individual cubies. + // A cubie at (100,0,0) rotating around Y axis: + // Needs to move in arc. + // `rotateY(angle) translate(...)` -> Rotates axis then moves. + // Yes. `rotateY` first puts it on the rotated axis. + // So `rotate` then `translate`. + transform = `rotate${axis.toUpperCase()}(${layerRotation.value}deg) ${transform}` + } } -}) + + return { transform } +} @@ -356,65 +547,57 @@ const movingGroupStyle = computed(() => { height: 300px; perspective: 900px; pointer-events: auto; +} + +.cube-group { + width: 100%; + height: 100%; 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 */ + transition: transform 0.1s; } -.cube.moving { - transition: transform 0.2s ease-out; /* Layer snap transition */ -} - -.face { +.cubie { position: absolute; - width: 300px; - height: 300px; - border: 2px solid rgba(0,0,0,0.1); + width: 100px; + height: 100px; + top: 100px; /* Center it: 300/2 - 100/2 = 100 */ + left: 100px; + transform-style: preserve-3d; +} + +.sticker-face { + position: absolute; + width: 100px; + height: 100px; + border: 1px solid rgba(0,0,0,0.8); /* Plastic edge */ + box-sizing: border-box; display: flex; - flex-wrap: wrap; - /* Transparent background to allow seeing through split parts */ - background: transparent; + justify-content: center; + align-items: center; + backface-visibility: hidden; /* Optimization? Or we want to see inside? */ + /* We want solid cubies. So we need backfaces or 6 faces. */ + /* We are rendering 6 faces. */ } -.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-border { + width: 92%; + height: 92%; + border: 2px solid rgba(0,0,0,0.5); + border-radius: 8px; /* Rounded sticker */ + background: inherit; /* Sticker color */ + /* The face bg is the plastic color (black usually). */ + /* Here we set face bg to color directly. */ + /* Let's adjust: face bg = black. sticker-border bg = color. */ } -.sticker-wrapper { - width: 100%; - height: 100%; - background: black; /* The black plastic look */ - padding: 2px; /* Inner padding for sticker */ - box-sizing: border-box; -} +/* Face transforms relative to Cubie Center */ +.sticker-face.front { transform: rotateY(0deg) translateZ(50px); } +.sticker-face.back { transform: rotateY(180deg) translateZ(50px); } +.sticker-face.right { transform: rotateY(90deg) translateZ(50px); } +.sticker-face.left { transform: rotateY(-90deg) translateZ(50px); } +.sticker-face.up { transform: rotateX(90deg) translateZ(50px); } +.sticker-face.down { transform: rotateX(-90deg) translateZ(50px); } -.sticker { - width: 100%; - height: 100%; - border-radius: 2px; - box-shadow: inset 0 0 5px rgba(0,0,0,0.2); -}