diff --git a/14-css-animation-builder/index.html b/14-css-animation-builder/index.html new file mode 100644 index 0000000..0573d3e --- /dev/null +++ b/14-css-animation-builder/index.html @@ -0,0 +1,89 @@ + + + + + + CSS Animation Builder + + + +
+

CSS Animation Builder

+ +
+
+
+
+
+ +
+ +
+

Transforms

+ + + + + + + + + + +
+ + +
+

Animation

+ + + + + + + + + + +
+
+ +
+

Generated CSS

+ + +
+
+ + + + \ No newline at end of file diff --git a/14-css-animation-builder/script.js b/14-css-animation-builder/script.js new file mode 100644 index 0000000..0b67d40 --- /dev/null +++ b/14-css-animation-builder/script.js @@ -0,0 +1,162 @@ + + +const box = document.getElementById('preview-box'); +const output = document.getElementById('output'); +const copyBtn = document.getElementById('copy-btn'); + +const controls = { + tx: document.getElementById('tx'), + ty: document.getElementById('ty'), + scale: document.getElementById('scale'), + rotate: document.getElementById('rotate'), + opacity: document.getElementById('opacity'), + duration: document.getElementById('duration'), + delay: document.getElementById('delay'), + easing: document.getElementById('easing'), + iterations: document.getElementById('iterations'), + direction: document.getElementById('direction') +}; + +function getSettings() { + const tx = Number(controls.tx.value) || 0; + const ty = Number(controls.ty.value) || 0; + const scale = Number(controls.scale.value) || 1; + const rotate = Number(controls.rotate.value) || 0; + const opacity = Number(controls.opacity.value); + const duration = Number(controls.duration.value) || 1; + const delay = Number(controls.delay.value) || 0; + const easing = controls.easing.value || 'ease'; + const iterationsRaw = Number(controls.iterations.value); + const iterations = iterationsRaw > 0 ? iterationsRaw : 1; + const direction = controls.direction.value || 'normal'; + + return { + tx, + ty, + scale, + rotate, + opacity, + duration, + delay, + easing, + iterations, + direction + }; +} + +function ensureStyleTag() { + let styleTag = document.getElementById('dynamic-keyframes'); + if (!styleTag) { + styleTag = document.createElement('style'); + styleTag.id = 'dynamic-keyframes'; + document.head.appendChild(styleTag); + } + return styleTag; +} + +function buildCSS(settings) { + const { + tx, + ty, + scale, + rotate, + opacity, + duration, + delay, + easing, + iterations, + direction + } = settings; + + const fromTransform = 'translate(0px, 0px) scale(1) rotate(0deg)'; + const toTransform = `translate(${tx}px, ${ty}px) scale(${scale}) rotate(${rotate}deg)`; + const fromOpacity = 1; + const toOpacity = opacity; + + const animationLine = `animation: myAnim ${duration}s ${easing} ${delay}s ${iterations} ${direction};`; + + const keyframes = ` +@keyframes myAnim { + from { + transform: ${fromTransform}; + opacity: ${fromOpacity}; + } + to { + transform: ${toTransform}; + opacity: ${toOpacity}; + } +} +`.trim(); + + const css = `.my-element { + ${animationLine} +} + +${keyframes} +`; + + return { css, keyframes, animationLine }; +} + +function applyAnimation() { + const settings = getSettings(); + const { css, keyframes, animationLine } = buildCSS(settings); + + // Restart animation + box.style.animation = 'none'; + // Force reflow so the browser picks up the reset + void box.offsetWidth; + + box.style.animation = `myAnim ${settings.duration}s ${settings.easing} ${settings.delay}s ${settings.iterations} ${settings.direction}`; + + const styleTag = ensureStyleTag(); + styleTag.textContent = keyframes; + + output.value = css; +} + +function attachListeners() { + Object.values(controls).forEach((el) => { + const eventName = el.tagName === 'SELECT' ? 'change' : 'input'; + el.addEventListener(eventName, applyAnimation); + }); + + copyBtn.addEventListener('click', () => { + const text = output.value.trim(); + if (!text) return; + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(() => flashCopyState(), () => fallbackCopy(text)); + } else { + fallbackCopy(text); + } + }); +} + +function fallbackCopy(text) { + output.focus(); + output.select(); + try { + document.execCommand('copy'); + flashCopyState(); + } catch (err) { + // ignore + } finally { + output.setSelectionRange(0, 0); + output.blur(); + } +} + +function flashCopyState() { + const original = copyBtn.textContent; + copyBtn.textContent = 'copied'; + copyBtn.disabled = true; + + setTimeout(() => { + copyBtn.textContent = original; + copyBtn.disabled = false; + }, 1200); +} + +attachListeners(); +applyAnimation(); \ No newline at end of file diff --git a/14-css-animation-builder/styles.css b/14-css-animation-builder/styles.css new file mode 100644 index 0000000..4e97fb4 --- /dev/null +++ b/14-css-animation-builder/styles.css @@ -0,0 +1,353 @@ +:root { + color-scheme: dark; + --bg: #050816; + --panel: #0f172a; + --panel-soft: #111827; + --accent: #22c55e; + --accent-soft: #16a34a33; + --text: #e5e7eb; + --muted: #9ca3af; + --border: #1f2937; + --radius: 12px; + --shadow-soft: 0 18px 45px rgba(0, 0, 0, 0.7); + --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", + "Segoe UI", sans-serif; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + height: 100%; +} + +body { + font-family: var(--font-sans); + background: radial-gradient(circle at top, #1e293b 0, #020617 45%, #000 100%); + color: var(--text); + display: flex; + align-items: stretch; + justify-content: center; + padding: 24px; +} + +main.container { + width: 100%; + max-width: 1100px; + background: linear-gradient(145deg, #020617 0, #020617 40%, #020617 100%); + border-radius: 20px; + box-shadow: var(--shadow-soft); + border: 1px solid rgba(148, 163, 184, 0.15); + padding: 24px; + display: grid; + grid-template-rows: auto auto auto; + gap: 20px; +} + +@media (min-width: 960px) { + main.container { + grid-template-rows: auto 1fr auto; + grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.3fr); + grid-template-areas: + "title title" + "preview controls" + "output output"; + } +} + +h1 { + font-size: 1.5rem; + margin: 0 0 8px; + letter-spacing: 0.04em; + display: flex; + align-items: center; + gap: 10px; +} + +h1::before { + content: ""; + width: 10px; + height: 22px; + border-radius: 999px; + background: linear-gradient(180deg, #22c55e, #22c55e33); +} + +.preview-section { + grid-area: preview; + background: radial-gradient(circle at top left, #0f172a, #020617 55%); + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.18); + padding: 18px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.preview-section h2 { + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--muted); + margin: 0; +} + +.preview-stage { + flex: 1; + min-height: 220px; + border-radius: 14px; + background: radial-gradient(circle at 20% 0, #22c55e22, transparent 60%), + radial-gradient(circle at 80% 120%, #22c55e11, transparent 60%); + border: 1px solid rgba(15, 23, 42, 0.9); + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.box { + width: 120px; + height: 120px; + border-radius: 26px; + background: conic-gradient( + from 160deg, + #bbf7d0 0, + #22c55e 40%, + #4ade80 65%, + #22c55e 100% + ); + box-shadow: 0 20px 45px rgba(34, 197, 94, 0.37); + transform-origin: center; +} + +.preview-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; + color: var(--muted); +} + +.preview-meta span { + opacity: 0.9; +} + +.preview-pill { + padding: 4px 10px; + border-radius: 999px; + border: 1px solid rgba(148, 163, 184, 0.3); + background: rgba(15, 23, 42, 0.7); +} + +/* Controls */ + +.controls-section { + grid-area: controls; + display: grid; + gap: 16px; + background: radial-gradient(circle at top, #020617, #020617 60%); + padding: 18px; + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.2); +} + +.group { + background: linear-gradient(145deg, #020617, #020617); + border-radius: 14px; + padding: 14px 14px 10px; + border: 1px solid rgba(30, 64, 175, 0.55); +} + +.group h2 { + font-size: 0.9rem; + margin: 0 0 10px; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--muted); +} + +.controls-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 10px 14px; +} + +@media (min-width: 720px) { + .controls-grid { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } +} + +.control { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.78rem; +} + +.control-line { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.control label { + color: var(--muted); +} + +.control span.value { + color: var(--accent); + font-variant-numeric: tabular-nums; +} + +input[type="range"] { + width: 100%; + appearance: none; + height: 4px; + border-radius: 999px; + background: #020617; + outline: none; +} + +input[type="range"]::-webkit-slider-thumb { + appearance: none; + width: 16px; + height: 16px; + border-radius: 999px; + background: var(--accent); + box-shadow: 0 0 0 4px var(--accent-soft); + border: 2px solid #022c22; + cursor: pointer; +} + +input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 999px; + background: var(--accent); + box-shadow: 0 0 0 4px var(--accent-soft); + border: 2px solid #022c22; + cursor: pointer; +} + +input[type="number"], +select { + width: 100%; + padding: 6px 8px; + border-radius: 8px; + border: 1px solid rgba(55, 65, 81, 0.9); + background: rgba(15, 23, 42, 0.95); + color: var(--text); + font-size: 0.8rem; +} + +input[type="number"]:focus, +select:focus { + outline: 1px solid var(--accent); + border-color: var(--accent); +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + appearance: none; + margin: 0; +} + +/* Output */ + +.output-section { + grid-area: output; + background: radial-gradient(circle at top, #020617, #020617 70%); + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.25); + padding: 16px 18px 18px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.output-section h2 { + font-size: 0.9rem; + margin: 0; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--muted); +} + +textarea#output { + width: 100%; + min-height: 130px; + max-height: 240px; + resize: vertical; + border-radius: 10px; + border: 1px solid rgba(31, 41, 55, 0.9); + background: #020617; + color: #e5e7eb; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; + font-size: 0.8rem; + padding: 10px 11px; + line-height: 1.5; + white-space: pre; +} + +button#copy-btn { + align-self: flex-end; + margin-top: 4px; + padding: 7px 13px; + border-radius: 999px; + border: 1px solid rgba(34, 197, 94, 0.9); + background: radial-gradient(circle at top, #22c55e, #16a34a); + color: #022c22; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + letter-spacing: 0.03em; +} + +button#copy-btn:hover { + filter: brightness(1.08); +} + +button#copy-btn:active { + transform: translateY(1px); +} + + +@media (max-width: 768px) { + body { + padding: 16px 10px; + } + + main.container { + padding: 16px 14px; + gap: 16px; + max-width: 100%; + display: flex; + flex-direction: column; + } + + .preview-section, + .controls-section, + .output-section { + padding: 14px; + } + + .preview-stage { + min-height: 200px; + } + + textarea#output { + min-height: 160px; + } +} diff --git a/xx-dice/index.html b/xx-dice/index.html index 7c333e4..6215a73 100644 --- a/xx-dice/index.html +++ b/xx-dice/index.html @@ -75,14 +75,30 @@ font-size: 16px; cursor: pointer; } + + button:focus-visible { + outline: 3px solid #61dafb; + outline-offset: 2px; + } + + .result { + margin-top: 16px; + font-size: 18px; + } @@ -126,7 +165,10 @@
2
- + +

+ Pulsa el botón para lanzar el dado. +

\ No newline at end of file