- for (var i = 1; i <= 9; i++)
.stretcher-cell
.view
.ball
//- not crucial swipe-aware structure
.layout-reference
each rowVal in [-1, 0, 1]
each colVal in [-1, 0, 1]
span(data-point=`(${rowVal}, ${colVal})`)
//- config menu
.config-menu
label.config-menu__toggler
input(type="checkbox" name="toggle-config-menu" id="toggle-config-menu")
span.icon ⚙
form.config-menu__form
fieldset
legend Value Type
label
input(type="radio" name="valueType" value="discrete" checked)
span Discrete
label
input(type="radio" name="valueType" value="continuous")
span Continuous
fieldset
legend Snap to Points
label
input(type="radio" name="snap" value="none")
span None
label
input(type="radio" name="snap" value="center" checked)
span Center
label
input(type="radio" name="snap" value="all")
span All
fieldset#optimized-for-diagonal-swipe
legend Optimization
label
input(type="checkbox" name="diagonal" id="checkbox-diagonal")
span Optimize for diagonal swipes
.info-icon
i.icon-info(title="Reduces the L-shaped motion when swiping diagonally")
View Compiled
:root {
--x: 0;
--y: 0;
--discrete-x: 0;
--discrete-y: 0;
--continuous-x: 0;
--continuous-y: 0;
--swipe-displacement: max(25vmin, 12.5vh);
}
html,
body {
overscroll-behavior: contain;
}
html {
scroll-snap-type: both mandatory;
scroll-snap-stop: always;
/* hide scrollbar */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
}
body {
margin: 0;
font-family: Helvetica, Arial, sans-serif;
--x-offset: calc(10rem + 10lvw);
--y-offset: calc(10rem + 10lvh);
--scroll-orientation: -1; /* [-1, 1] */
scrollbar-width: none;
-ms-overflow-style: none;
position: relative;
flex-grow: 0;
flex-shrink: 0;
display: grid;
height: calc(90lvh + var(--y-offset));
width: calc(90lvw + var(--x-offset));
grid-template-columns: calc(var(--x-offset) / 2) 90lvw calc(
var(--x-offset) / 2
);
grid-template-rows: calc(var(--y-offset) / 2) 90lvh calc(var(--y-offset) / 2);
}
body::-webkit-scrollbar {
width: 0;
height: 0;
}
.stretcher-cell {
box-shadow: inset 0 0 0 1px gray;
}
@keyframes discrete-horizontal-motion {
0% {
--discrete-x: -1;
}
50% {
--discrete-x: 0;
}
100% {
--discrete-x: 1;
}
}
@keyframes discrete-vertical-motion {
0% {
--discrete-y: -1;
}
50% {
--discrete-y: 0;
}
100% {
--discrete-y: 1;
}
}
@property --continuous-x {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --continuous-y {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@keyframes continuous-horizontal-motion {
0% {
--continuous-x: -1;
}
50% {
--continuous-x: 0;
}
100% {
--continuous-x: 1;
}
}
@keyframes continuous-vertical-motion {
0% {
--continuous-y: -1;
}
50% {
--continuous-y: 0;
}
100% {
--continuous-y: 1;
}
}
.view {
position: fixed;
top: 0;
left: 0;
width: 100dvw;
height: 100dvh;
pointer-events: none;
}
@function abs($val) {
@return max(#{$val}, -1 * #{$val});
}
@function sign($val) {
@return calc(#{$val}/#{abs(#{$val})});
}
@function delayed($val) {
// returns 0 until (x, y) value is >= 1.
@return calc(10000 * (max(0.9999, #{$val}) - 0.9999));
}
.ball {
--ball-size: max(5vmax, 7.5vh);
position: absolute;
width: var(--ball-size);
height: var(--ball-size);
border-radius: 50%;
background: dodgerblue;
top: calc(50dvh - var(--ball-size) / 2);
left: calc(50dvw - var(--ball-size) / 2);
flex-grow: var(--x);
flex-shrink: var(--y);
transition: transform 125ms ease-out;
}
/* ------------------------------------------------------------ */
/* ----------------- Swipe behavior variations ---------------- */
/* ------------------------------------------------------------ */
body {
/* Discrete vs Continuous */
&:has(.config-menu input[name="valueType"][value="discrete"]:checked):not(
:has(.config-menu #checkbox-diagonal:checked)
) {
animation: discrete-horizontal-motion 1ms linear,
discrete-vertical-motion 1ms linear;
animation-timeline: scroll(nearest inline), scroll(nearest block);
transition: --discrete-x 50ms ease-out, --discrete-y 50ms ease-out;
& > * {
--x: var(--discrete-x);
--y: var(--discrete-y);
}
.ball {
transform: translate(
calc(-1 * var(--swipe-displacement) * var(--x)),
calc(-1 * var(--swipe-displacement) * var(--y))
);
}
}
&:has(.config-menu input[name="valueType"][value="continuous"]:checked),
&:has(.config-menu #checkbox-diagonal:checked) {
animation: continuous-horizontal-motion 1ms linear,
continuous-vertical-motion 1ms linear;
animation-timeline: scroll(nearest inline), scroll(nearest block);
transition: --continuous-x 50ms ease-out, --continuous-y 50ms ease-out;
& > * {
--x: var(--continuous-x);
--y: var(--continuous-y);
}
.ball {
transform: translate(
calc(-1 * var(--swipe-displacement) * var(--x)),
calc(-1 * var(--swipe-displacement) * var(--y))
);
}
}
/* Grid snap */
&:has(.config-menu input[name="snap"][value="none"]:checked):not(
:has(.config-menu #checkbox-diagonal:checked)
) {
}
&:has(.config-menu input[name="snap"][value="center"]:checked),
&:has(.config-menu #checkbox-diagonal:checked) {
.stretcher-cell:nth-of-type(5) {
background: #ccc;
scroll-snap-align: center center;
}
}
&:has(.config-menu input[name="snap"][value="all"]:checked):not(
:has(.config-menu #checkbox-diagonal:checked)
) {
.stretcher-cell {
background: #ccc;
scroll-snap-align: center center;
}
}
/* Optimized for Diagonal Swipe */
&:has(.config-menu #checkbox-diagonal:checked) {
.layout-reference {
&::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
45deg,
transparent 49.875%,
navy 49.875%,
navy 50.125%,
transparent 50.125%
);
opacity: 0.1;
}
&::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
-45deg,
transparent 49.875%,
navy 49.875%,
navy 50.125%,
transparent 50.125%
);
opacity: 0.1;
}
}
.ball {
--sum: min(
1,
pow(#{abs(var(--x, 0))}, 0.05) + pow(#{abs(var(--y, 0))}, 0.05)
);
--delay-result: #{delayed(var(--sum))};
transform: translate(
calc(
-1 * var(--swipe-displacement) * #{sign(var(--x, 0))} *
var(--delay-result)
),
calc(
-1 * var(--swipe-displacement) * #{sign(var(--y, 0))} *
var(--delay-result)
)
);
}
}
}
/* ------------------------------------------------------------ */
/* --------------------- Aux UI component --------------------- */
/* ------------------------------------------------------------ */
.layout-reference {
position: fixed;
width: calc(2 * var(--swipe-displacement));
height: calc(2 * var(--swipe-displacement));
top: calc(50% - var(--swipe-displacement));
left: calc(50% - var(--swipe-displacement));
background: #d7ecff; // "lightdodgerblue"
font-family: monospace;
font-variant-numeric: tabular-nums;
margin: auto;
pointer-events: none;
display: grid;
grid-template-columns: repeat(3, auto);
grid-template-rows: repeat(3, auto);
justify-content: space-between;
align-content: space-between;
mix-blend-mode: multiply;
span {
position: relative;
text-align: center;
}
span::before {
position: absolute;
white-space: nowrap;
transform: translate(-50%, -50%);
content: attr(data-point);
}
}
/* ------------------------------------------------------------ */
/* --------------- Config menu - swipe behavior --------------- */
/* ------------------------------------------------------------ */
.config-menu {
--config-menu_bg-color: #333;
--config-menu_text-color: #fff;
--config-menu_accent-color: dodgerblue;
--config-menu_border-radius: 4px;
--config-menu_offset: 1ch;
position: fixed;
top: var(--config-menu_offset);
right: var(--config-menu_offset);
z-index: 99999999;
padding-left: var(--config-menu_offset);
& > * {
top: 0;
right: 0;
background: var(--config-menu_bg-color);
color: var(--config-menu_text-color);
border-radius: var(--config-menu_border-radius);
box-shadow: 0 0.5ch 1ch rgba(0 0 0 / 0.25), 0 1ch 3ch rgba(0 0 0 / 0.25);
}
}
.config-menu__toggler {
position: absolute;
display: flex;
font-size: 2em;
line-height: 0.5em;
padding: 0.5ch;
cursor: pointer;
z-index: 1;
&:has(input:checked) {
box-shadow: none;
.icon {
rotate: 25deg;
}
& + .config-menu__form {
transform: translateX(0);
}
}
input {
/* Add if not using autoprefixer */
-webkit-appearance: none;
/* Remove most all native input styles */
appearance: none;
/* For iOS < 15 */
background-color: var(--config-menu_bg-color);
/* Not removed via appearance */
margin: 0;
width: 0;
height: 0;
}
.icon {
display: inline-block;
transition: 125ms ease-out;
user-select: none;
}
&:active .icon {
rotate: 25deg;
}
}
.config-menu__form {
font-family: monospace;
font-weight: 300;
font-size: 12px;
padding: 1ch;
width: fit-content;
transition: transform 250ms cubic-bezier(0.28, 0.95, 0.28, 0.95);
transform: translateX(calc(100% + 5 * var(--config-menu_offset)));
fieldset {
border: 1px solid #ccc;
padding: 10px;
&:not(:last-of-type) {
margin-bottom: 1em;
}
legend {
font-weight: bold;
}
label {
display: flex;
margin-bottom: 5px;
& > * {
flex-shrink: 0;
}
span {
margin-left: 5px;
}
}
}
&:has(#checkbox-diagonal:checked)
fieldset:not(#optimized-for-diagonal-swipe) {
opacity: 0.5;
cursor: not-allowed;
label,
input {
pointer-events: none;
}
}
.info-icon {
display: inline-block;
position: relative;
i.icon-info {
cursor: pointer;
&:before {
content: "ⓘ";
text-align: center;
margin-left: 0.5ch;
}
&:hover + .tooltip {
visibility: visible;
opacity: 1;
}
}
.tooltip {
visibility: hidden;
opacity: 0;
position: absolute;
left: 50%;
bottom: 100%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
padding: 5px 10px;
border-radius: 5px;
transition: opacity 0.3s ease;
&:before {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
}
}
/*
Repurpused radio buttons from Stephanie Eckles
https://moderncss.dev/pure-css-custom-styled-radio-buttons/
*/
input:is([type="radio"], [type="checkbox"]) {
/* Add if not using autoprefixer */
-webkit-appearance: none;
/* Remove most all native input styles */
appearance: none;
/* For iOS < 15 */
background-color: var(--config-menu_bg-color);
/* Not removed via appearance */
margin: 0;
overflow: hidden;
font: inherit;
color: currentColor;
width: 1.15em;
height: 1.15em;
border: 0.15em solid currentColor;
transform: translateY(-0.075em);
display: grid;
place-content: center;
&::before {
width: 0.65em;
height: 0.65em;
transform: scale(0);
transition: 125ms transform ease-in-out;
/* Windows High Contrast Mode */
background-color: CanvasText;
}
&:focus {
outline: max(2px, 0.15em) solid currentColor;
outline-offset: max(2px, 0.15em);
}
&:checked::before {
transform: scale(1);
}
}
input[type="radio"] {
border-radius: 50%;
&::before {
content: "";
border-radius: inherit;
box-shadow: inset 1em 1em var(--config-menu_accent-color);
}
}
input[type="checkbox"] {
border-radius: 2px;
&::before {
content: "✓";
border-radius: none;
font-size: 1.5em;
line-height: 1.1ch;
background: var(--config-menu_accent-color);
}
}
}
View Compiled
/*
/ _ \
\_\(_)/_/
_//"\\_ Max
/ \
CSS-only, JavaScript-free code!
-------------------------------
Preview for:
Desert Racer 🏜️: World's First CSS-only Swipe-Aware Game!
Article at:
https://dev.to/warkentien2/desert-racer-worlds-first-css-only-swipe-aware-game-4j0h
by @warkentien2
MIT License
Copyright (c) 2023 Philip Warkentien II
*/
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.