openvibers/openvibe.shoes Commits
Shoes' motif: a sneaker in one stroke, the stores' tags arriving (#1)
Changes
@@@ -84,3 +84,23 @@.crumbs a { color: inherit; }@media (max-width: 900px) { .sh-search { grid-template-columns: 1fr 1fr; } }@media (max-width: 640px) { .sh-search { grid-template-columns: 1fr; } .sh-wrap { width: min(100% - 24px, 1180px); } }++++/* The room's motif: one stroke draws the shoe, then three stores' tags slide in and the lowest lights up. Motion++ follows the network's tokens (Calm stills it); reduced motion shows the finished picture. */++.sh-motif { position: relative; width: min(100%, 400px); aspect-ratio: 16 / 11; margin: 0 auto; color: var(--room-accent); }++.sh-shoe { position: absolute; inset: 10% 6% auto; width: 84%; }++.sh-shoe path { fill: none; stroke: currentColor; stroke-width: 3.2; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 100;++ animation: shDraw calc(var(--ov-dur-4, .52s) * 3.5) var(--ov-ease-in-out, ease-in-out) both; }++@keyframes shDraw { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }++.sh-mt { position: absolute; display: grid; gap: 1px; padding: 7px 12px; border-radius: 10px; background: var(--ovk-panel, #121826); border: 1px solid var(--ovk-line, rgba(255,255,255,.1));++ box-shadow: 0 14px 30px -18px rgba(0, 0, 0, .7); color: var(--ovk-text, #e8ecf3); font-variant-numeric: tabular-nums;++ animation: shTag var(--ov-dur-4, .52s) var(--ov-ease-spring, cubic-bezier(.34, 1.56, .64, 1)) both; }++.sh-mt small { font-size: 11px; color: var(--ovk-muted, #93a0b4); }++.sh-mt b { font-size: 18px; }++.sh-mt-1 { right: 0; top: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3); }++.sh-mt-2 { right: 18%; bottom: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3.4); }++.sh-mt-3 { left: 4%; bottom: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3.8); }++.sh-mt.is-best { border-color: var(--room-accent); box-shadow: 0 0 0 1px var(--room-accent), 0 14px 30px -18px rgba(0, 0, 0, .7); }++.sh-mt.is-best b { color: var(--room-strong, var(--room-accent)); }++@keyframes shTag { from { opacity: 0; transform: translateX(24px); } }++@media (prefers-reduced-motion: reduce) { .sh-shoe path, .sh-mt { animation: none; } }@@@ -175,6 +175,18 @@ function searchForm({ q = '', facets = {}, action = '/shoes', values = {} }) {}// ── Home ────────────────────────────────────────────────────++/**++ * The room's motif (rooms.js: "a shoe drawn as one line, price tags from several stores sliding in"): a sneaker in a++ * single stroke that draws itself, then three stores' tags arrive and the lowest one lights up. Drawn, not a photo.++ */++function shoeMotif() {++ const tag = (store, price, n, best) => `<span class="sh-mt sh-mt-${n}${best ? ' is-best' : ''}"><small>${store}</small><b>${price}</b></span>`;++ return `<div class="sh-motif" aria-hidden="true">++<svg class="sh-shoe" viewBox="0 0 320 170"><path pathLength="100" d="M30 140 C 26 120, 28 100, 36 90 C 46 82, 64 84, 84 92 C 96 96, 104 88, 110 74 C 114 64, 122 58, 132 60 L 140 63 C 162 78, 200 96, 240 104 C 272 110, 298 116, 304 130 C 306 138, 302 142, 294 142 L 36 142 C 32 142, 30 141, 30 140 Z M 26 142 L 26 151 C 26 156, 30 158, 36 158 L 296 158 C 304 158, 310 152, 308 142 M 150 76 L 160 67 M 168 84 L 178 75 M 186 92 L 196 83 M 70 124 C 120 120, 180 113, 244 120 M 36 96 L 28 98"/></svg>++${tag('Store A', '$129', 1, false)}${tag('Store B', '$112', 2, false)}${tag('Store C', '$98', 3, true)}++</div>`;++}++function home({ facets = {}, dropping = [], dropsToday = [], popular = [], curveModel = null, curve = null, sceneDrop = null, jobs = [], now = Date.now() } = {}) {const brands = (facets.brands || []).slice(0, 5);const priceDrops = dropsToday.filter((m) => m.drop_amount).slice(0, 8);@@@ -217,6 +229,7 @@ function home({ facets = {}, dropping = [], dropsToday = [], popular = [], curvelede: 'One page per shoe, every store\'s offer in one all-in price, the price history from Shoes\' own daily snapshots, and a watch that tells you when your size drops below your price.',search: { action: '/shoes', name: 'q', placeholder: 'Search a shoe, a brand or a model', label: 'Search shoes', button: 'Search', hints: ['Air Max 90', 'Samba', 'Pegasus', 'New Balance 990'] },chips: brands.map((b) => ({ label: b.value, href: `/shoes?brand=${encodeURIComponent(b.value)}` })),++ motif: shoeMotif(),},proof: [{ value: facets.models, label: facets.models === 1 ? 'model tracked' : 'models tracked' },
@@ -84,3 +84,23 @@
@@ -84,3 +84,23 @@
.crumbs a { color: inherit; }
.crumbs a { color: inherit; }
@media (max-width: 900px) { .sh-search { grid-template-columns: 1fr 1fr; } }
@media (max-width: 900px) { .sh-search { grid-template-columns: 1fr 1fr; } }
@media (max-width: 640px) { .sh-search { grid-template-columns: 1fr; } .sh-wrap { width: min(100% - 24px, 1180px); } }
@media (max-width: 640px) { .sh-search { grid-template-columns: 1fr; } .sh-wrap { width: min(100% - 24px, 1180px); } }
+
+/* The room's motif: one stroke draws the shoe, then three stores' tags slide in and the lowest lights up. Motion
+ follows the network's tokens (Calm stills it); reduced motion shows the finished picture. */
+.sh-motif { position: relative; width: min(100%, 400px); aspect-ratio: 16 / 11; margin: 0 auto; color: var(--room-accent); }
+.sh-shoe { position: absolute; inset: 10% 6% auto; width: 84%; }
+.sh-shoe path { fill: none; stroke: currentColor; stroke-width: 3.2; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 100;
+ animation: shDraw calc(var(--ov-dur-4, .52s) * 3.5) var(--ov-ease-in-out, ease-in-out) both; }
+@keyframes shDraw { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
+.sh-mt { position: absolute; display: grid; gap: 1px; padding: 7px 12px; border-radius: 10px; background: var(--ovk-panel, #121826); border: 1px solid var(--ovk-line, rgba(255,255,255,.1));
+ box-shadow: 0 14px 30px -18px rgba(0, 0, 0, .7); color: var(--ovk-text, #e8ecf3); font-variant-numeric: tabular-nums;
+ animation: shTag var(--ov-dur-4, .52s) var(--ov-ease-spring, cubic-bezier(.34, 1.56, .64, 1)) both; }
+.sh-mt small { font-size: 11px; color: var(--ovk-muted, #93a0b4); }
+.sh-mt b { font-size: 18px; }
+.sh-mt-1 { right: 0; top: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3); }
+.sh-mt-2 { right: 18%; bottom: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3.4); }
+.sh-mt-3 { left: 4%; bottom: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3.8); }
+.sh-mt.is-best { border-color: var(--room-accent); box-shadow: 0 0 0 1px var(--room-accent), 0 14px 30px -18px rgba(0, 0, 0, .7); }
+.sh-mt.is-best b { color: var(--room-strong, var(--room-accent)); }
+@keyframes shTag { from { opacity: 0; transform: translateX(24px); } }
+@media (prefers-reduced-motion: reduce) { .sh-shoe path, .sh-mt { animation: none; } }
@@ -175,6 +175,18 @@ function searchForm({ q = '', facets = {}, action = '/shoes', values = {} }) {
@@ -175,6 +175,18 @@ function searchForm({ q = '', facets = {}, action = '/shoes', values = {} }) {
}
}
// ── Home ────────────────────────────────────────────────────
// ── Home ────────────────────────────────────────────────────
+/**
+ * The room's motif (rooms.js: "a shoe drawn as one line, price tags from several stores sliding in"): a sneaker in a
+ * single stroke that draws itself, then three stores' tags arrive and the lowest one lights up. Drawn, not a photo.
+ */
+function shoeMotif() {
+ const tag = (store, price, n, best) => `<span class="sh-mt sh-mt-${n}${best ? ' is-best' : ''}"><small>${store}</small><b>${price}</b></span>`;
+ return `<div class="sh-motif" aria-hidden="true">
+<svg class="sh-shoe" viewBox="0 0 320 170"><path pathLength="100" d="M30 140 C 26 120, 28 100, 36 90 C 46 82, 64 84, 84 92 C 96 96, 104 88, 110 74 C 114 64, 122 58, 132 60 L 140 63 C 162 78, 200 96, 240 104 C 272 110, 298 116, 304 130 C 306 138, 302 142, 294 142 L 36 142 C 32 142, 30 141, 30 140 Z M 26 142 L 26 151 C 26 156, 30 158, 36 158 L 296 158 C 304 158, 310 152, 308 142 M 150 76 L 160 67 M 168 84 L 178 75 M 186 92 L 196 83 M 70 124 C 120 120, 180 113, 244 120 M 36 96 L 28 98"/></svg>
+${tag('Store A', '$129', 1, false)}${tag('Store B', '$112', 2, false)}${tag('Store C', '$98', 3, true)}
+</div>`;
+}
+
function home({ facets = {}, dropping = [], dropsToday = [], popular = [], curveModel = null, curve = null, sceneDrop = null, jobs = [], now = Date.now() } = {}) {
function home({ facets = {}, dropping = [], dropsToday = [], popular = [], curveModel = null, curve = null, sceneDrop = null, jobs = [], now = Date.now() } = {}) {
const brands = (facets.brands || []).slice(0, 5);
const brands = (facets.brands || []).slice(0, 5);
const priceDrops = dropsToday.filter((m) => m.drop_amount).slice(0, 8);
const priceDrops = dropsToday.filter((m) => m.drop_amount).slice(0, 8);
@@ -217,6 +229,7 @@ function home({ facets = {}, dropping = [], dropsToday = [], popular = [], curve
@@ -217,6 +229,7 @@ function home({ facets = {}, dropping = [], dropsToday = [], popular = [], curve
lede: 'One page per shoe, every store\'s offer in one all-in price, the price history from Shoes\' own daily snapshots, and a watch that tells you when your size drops below your price.',
lede: 'One page per shoe, every store\'s offer in one all-in price, the price history from Shoes\' own daily snapshots, and a watch that tells you when your size drops below your price.',
search: { action: '/shoes', name: 'q', placeholder: 'Search a shoe, a brand or a model', label: 'Search shoes', button: 'Search', hints: ['Air Max 90', 'Samba', 'Pegasus', 'New Balance 990'] },
search: { action: '/shoes', name: 'q', placeholder: 'Search a shoe, a brand or a model', label: 'Search shoes', button: 'Search', hints: ['Air Max 90', 'Samba', 'Pegasus', 'New Balance 990'] },
chips: brands.map((b) => ({ label: b.value, href: `/shoes?brand=${encodeURIComponent(b.value)}` })),
chips: brands.map((b) => ({ label: b.value, href: `/shoes?brand=${encodeURIComponent(b.value)}` })),
+ motif: shoeMotif(),
},
},
proof: [
proof: [
{ value: facets.models, label: facets.models === 1 ? 'model tracked' : 'models tracked' },
{ value: facets.models, label: facets.models === 1 ? 'model tracked' : 'models tracked' },