Minor improvements

This commit is contained in:
Andras Schmelczer 2026-05-26 20:59:01 +01:00
parent 0fddad6b45
commit 4e24df1511
9 changed files with 175 additions and 100 deletions

View file

@ -4,14 +4,14 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta <meta
name="viewport" name="viewport"
content="width=device-width,initial-scale=1,viewport-fit=cover" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
/> />
<meta name="theme-color" content="#10151f" /> <meta name="theme-color" content="#10151f" />
<meta name="robots" content="index,follow" /> <meta name="robots" content="index,follow" />
<meta name="author" content="Andras Schmelczer" /> <meta name="author" content="Andras Schmelczer" />
<meta <meta
name="description" name="description"
content="Plant colour, fold gestures with mirrors, and watch small agents turn each brushstroke into a shifting WebGPU garden." content="Plant colours in your fleeting garden and watch them flow as time passes. Immerse yourself into shaping nature and nature shaping your work."
/> />
<link rel="canonical" href="https://schmelczer.dev/fleeting/" /> <link rel="canonical" href="https://schmelczer.dev/fleeting/" />
@ -22,7 +22,7 @@
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta <meta
property="og:description" property="og:description"
content="Plant colour, fold gestures with mirrors, and watch small agents turn each brushstroke into a shifting WebGPU garden." content="Plant colours in your fleeting garden and watch them flow as time passes. Immerse yourself into shaping nature and nature shaping your work."
/> />
<meta property="og:url" content="https://schmelczer.dev/fleeting/" /> <meta property="og:url" content="https://schmelczer.dev/fleeting/" />
<meta property="og:image" content="https://schmelczer.dev/fleeting/og-image.jpg" /> <meta property="og:image" content="https://schmelczer.dev/fleeting/og-image.jpg" />
@ -35,7 +35,7 @@
<meta name="twitter:title" content="Fleeting Garden" /> <meta name="twitter:title" content="Fleeting Garden" />
<meta <meta
name="twitter:description" name="twitter:description"
content="Plant colour, fold gestures with mirrors, and watch small agents turn each brushstroke into a shifting WebGPU garden." content="Plant colours in your fleeting garden and watch them flow as time passes. Immerse yourself into shaping nature and nature shaping your work."
/> />
<meta name="twitter:image" content="https://schmelczer.dev/fleeting/og-image.jpg" /> <meta name="twitter:image" content="https://schmelczer.dev/fleeting/og-image.jpg" />
<meta name="twitter:image:alt" content="Fleeting Garden social preview image." /> <meta name="twitter:image:alt" content="Fleeting Garden social preview image." />
@ -46,7 +46,7 @@
"@type": "WebApplication", "@type": "WebApplication",
"name": "Fleeting Garden", "name": "Fleeting Garden",
"url": "https://schmelczer.dev/fleeting/", "url": "https://schmelczer.dev/fleeting/",
"description": "Plant colour, fold gestures with mirrors, and watch small agents turn each brushstroke into a shifting WebGPU garden.", "description": "Plant colours in your fleeting garden and watch them flow as time passes. Immerse yourself into shaping nature and nature shaping your work.",
"image": "https://schmelczer.dev/fleeting/og-image.jpg", "image": "https://schmelczer.dev/fleeting/og-image.jpg",
"applicationCategory": "DesignApplication", "applicationCategory": "DesignApplication",
"operatingSystem": "Any", "operatingSystem": "Any",
@ -91,7 +91,7 @@
<div class="splash" data-visible="true"> <div class="splash" data-visible="true">
<h1 class="splash-title">Fleeting Garden</h1> <h1 class="splash-title">Fleeting Garden</h1>
<p class="splash-description"> <p class="splash-description">
Tend it while you can. The garden returns to weather either way. Plant colours in your fleeting garden and watch them flow as time passes. Immerse yourself into shaping nature and nature shaping your work.
</p> </p>
<button class="start-button" type="button" disabled>Start</button> <button class="start-button" type="button" disabled>Start</button>
</div> </div>

View file

@ -28,10 +28,10 @@ const graphTuning = {
latencyHint: 'interactive', latencyHint: 'interactive',
outputFilterType: 'highpass', outputFilterType: 'highpass',
compressor: { compressor: {
thresholdDb: -18, thresholdDb: -22,
kneeDb: 18, kneeDb: 12,
ratio: 2.1, ratio: 4.5,
attackSeconds: 0.018, attackSeconds: 0.006,
releaseSeconds: 0.18, releaseSeconds: 0.18,
}, },
} as const; } as const;
@ -87,10 +87,12 @@ export class GardenAudioGraph {
const context = new AudioContextConstructor({ const context = new AudioContextConstructor({
latencyHint: graphTuning.latencyHint, latencyHint: graphTuning.latencyHint,
}); });
const outputBus = context.createGain();
const masterGain = context.createGain(); const masterGain = context.createGain();
const highPass = context.createBiquadFilter(); const highPass = context.createBiquadFilter();
const compressor = context.createDynamicsCompressor(); const compressor = context.createDynamicsCompressor();
outputBus.gain.value = 1;
masterGain.gain.value = 0; masterGain.gain.value = 0;
highPass.type = graphTuning.outputFilterType; highPass.type = graphTuning.outputFilterType;
highPass.frequency.value = outputHighPassFrequencyHz; highPass.frequency.value = outputHighPassFrequencyHz;
@ -100,15 +102,17 @@ export class GardenAudioGraph {
compressor.attack.value = graphTuning.compressor.attackSeconds; compressor.attack.value = graphTuning.compressor.attackSeconds;
compressor.release.value = graphTuning.compressor.releaseSeconds; compressor.release.value = graphTuning.compressor.releaseSeconds;
masterGain.connect(highPass); // Keep peak control independent from the user's volume slider.
outputBus.connect(highPass);
highPass.connect(compressor); highPass.connect(compressor);
compressor.connect(context.destination); compressor.connect(masterGain);
masterGain.connect(context.destination);
this.context = context; this.context = context;
this.masterGain = masterGain; this.masterGain = masterGain;
this.noiseBuffer = this.createNoiseBuffer(context); this.noiseBuffer = this.createNoiseBuffer(context);
this.createDelay(context, masterGain); this.createDelay(context, outputBus);
this.createBuses(context, masterGain); this.createBuses(context, outputBus);
return context; return context;
} }
@ -224,7 +228,7 @@ export class GardenAudioGraph {
} }
} }
private createDelay(context: AudioContext, masterGain: GainNode): void { private createDelay(context: AudioContext, outputBus: GainNode): void {
const delayInput = context.createGain(); const delayInput = context.createGain();
const delayNode = context.createDelay(graphTuning.delayMaxSeconds); const delayNode = context.createDelay(graphTuning.delayMaxSeconds);
const delayFeedback = context.createGain(); const delayFeedback = context.createGain();
@ -250,7 +254,7 @@ export class GardenAudioGraph {
delayFeedback.connect(delayNode); delayFeedback.connect(delayNode);
delayNode.connect(returnLowPass); delayNode.connect(returnLowPass);
returnLowPass.connect(delayOutput); returnLowPass.connect(delayOutput);
delayOutput.connect(masterGain); delayOutput.connect(outputBus);
this.delayInput = delayInput; this.delayInput = delayInput;
this.delayNode = delayNode; this.delayNode = delayNode;
@ -258,10 +262,10 @@ export class GardenAudioGraph {
this.delayOutput = delayOutput; this.delayOutput = delayOutput;
} }
private createBuses(context: AudioContext, masterGain: GainNode): void { private createBuses(context: AudioContext, outputBus: GainNode): void {
const eventBus = context.createGain(); const eventBus = context.createGain();
eventBus.gain.value = graphTuning.eventBusGain; eventBus.gain.value = graphTuning.eventBusGain;
eventBus.connect(masterGain); eventBus.connect(outputBus);
this.eventBus = eventBus; this.eventBus = eventBus;
this.pianoBuses.clear(); this.pianoBuses.clear();

84
src/config/eraser-size.ts Normal file
View file

@ -0,0 +1,84 @@
export interface CssPixelSize {
height: number;
width: number;
}
export const ERASER_SIZE_MIN = 24;
export const ERASER_SIZE_MAX = 480;
const ERASER_MAX_SHORT_SIDE_RATIO = 0.55;
const getNormalizedEraserSizeMax = (maxSize: number): number => {
const safeMaxSize = Number.isFinite(maxSize) ? Math.floor(maxSize) : ERASER_SIZE_MAX;
return Math.max(ERASER_SIZE_MIN, Math.min(ERASER_SIZE_MAX, safeMaxSize));
};
export const getElementCssPixelSize = (element: Element): CssPixelSize => {
const rect = element.getBoundingClientRect();
const { clientHeight = 0, clientWidth = 0 } = element as HTMLElement;
return {
height: rect.height || clientHeight,
width: rect.width || clientWidth,
};
};
export const getEraserSizeMaxForCssSize = ({ height, width }: CssPixelSize): number => {
const shortestSide = Math.min(height, width);
if (!Number.isFinite(shortestSide) || shortestSide <= 0) {
return ERASER_SIZE_MAX;
}
return getNormalizedEraserSizeMax(
Math.floor(shortestSide * ERASER_MAX_SHORT_SIDE_RATIO)
);
};
export const clampEraserSize = (
value: number,
maxSize = ERASER_SIZE_MAX,
fallbackSize = ERASER_SIZE_MIN
): number => {
const max = getNormalizedEraserSizeMax(maxSize);
const fallback = Number.isFinite(fallbackSize) ? fallbackSize : ERASER_SIZE_MIN;
const safeValue = Number.isFinite(value) ? value : fallback;
return Math.min(max, Math.max(ERASER_SIZE_MIN, Math.round(safeValue)));
};
export const getEffectiveEraserSize = (
size: number,
cssSize: CssPixelSize,
fallbackSize = ERASER_SIZE_MIN
): number => clampEraserSize(size, getEraserSizeMaxForCssSize(cssSize), fallbackSize);
export const getEraserSizeRatio = (size: number, maxSize = ERASER_SIZE_MAX): number => {
const max = getNormalizedEraserSizeMax(maxSize);
if (max === ERASER_SIZE_MIN) {
return 0;
}
return (clampEraserSize(size, max) - ERASER_SIZE_MIN) / (max - ERASER_SIZE_MIN);
};
const ERASER_SLIDER_MIN = 0;
const ERASER_SLIDER_MAX = 1;
const clampSliderRatio = (value: number): number => {
const safeValue = Number.isFinite(value) ? value : ERASER_SLIDER_MIN;
return Math.min(ERASER_SLIDER_MAX, Math.max(ERASER_SLIDER_MIN, safeValue));
};
export const getEraserSizeFromSliderRatio = (
sliderRatio: number,
maxSize = ERASER_SIZE_MAX
): number => {
const max = getNormalizedEraserSizeMax(maxSize);
return clampEraserSize(
ERASER_SIZE_MIN + (max - ERASER_SIZE_MIN) * clampSliderRatio(sliderRatio) ** 2,
max
);
};
export const getEraserSliderRatioFromSize = (
size: number,
maxSize = ERASER_SIZE_MAX
): number => Math.sqrt(getEraserSizeRatio(size, maxSize));

View file

@ -1,3 +1,4 @@
import { getEffectiveEraserSize } from '../config/eraser-size';
import { settings } from '../settings'; import { settings } from '../settings';
export class EraserPreview { export class EraserPreview {
@ -49,9 +50,15 @@ export class EraserPreview {
}; };
} }
if (this.previousSize !== settings.eraserSize) { const rect = this.canvas.getBoundingClientRect();
this.element.style.setProperty('--eraser-preview-size', `${settings.eraserSize}px`); const size = getEffectiveEraserSize(settings.eraserSize, {
this.previousSize = settings.eraserSize; height: rect.height || this.canvas.clientHeight,
width: rect.width || this.canvas.clientWidth,
});
if (this.previousSize !== size) {
this.element.style.setProperty('--eraser-preview-size', `${size}px`);
this.previousSize = size;
} }
if ( if (
@ -63,7 +70,6 @@ export class EraserPreview {
return; return;
} }
const rect = this.canvas.getBoundingClientRect();
const left = `${this.previewClientPosition.x - rect.left}px`; const left = `${this.previewClientPosition.x - rect.left}px`;
const top = `${this.previewClientPosition.y - rect.top}px`; const top = `${this.previewClientPosition.y - rect.top}px`;
if (this.previousLeft !== left) { if (this.previousLeft !== left) {

View file

@ -2,6 +2,7 @@ import { vec2 } from 'gl-matrix';
import { GardenAudio } from '../audio/garden-audio'; import { GardenAudio } from '../audio/garden-audio';
import { createGardenAudioConfig } from '../audio/garden-audio-config'; import { createGardenAudioConfig } from '../audio/garden-audio-config';
import { getEffectiveEraserSize, getElementCssPixelSize } from '../config/eraser-size';
import { activeVibe, settings } from '../settings'; import { activeVibe, settings } from '../settings';
import { DeltaTimeCalculator } from '../utils/delta-time-calculator'; import { DeltaTimeCalculator } from '../utils/delta-time-calculator';
import { rgbColorToCss, type RgbColor } from '../utils/rgb-color'; import { rgbColorToCss, type RgbColor } from '../utils/rgb-color';
@ -209,7 +210,11 @@ export default class GameLoop {
const runtimeSettings = { ...settings }; const runtimeSettings = { ...settings };
const introProgress = this.introPrompt.progress; const introProgress = this.introPrompt.progress;
const canvasPixelRatio = this.canvasPixelRatio; const canvasPixelRatio = this.canvasPixelRatio;
const eraserPixelSize = runtimeSettings.eraserSize * canvasPixelRatio; const eraserCssSize = getEffectiveEraserSize(
runtimeSettings.eraserSize,
getElementCssPixelSize(this.canvas)
);
const eraserPixelSize = eraserCssSize * canvasPixelRatio;
const isErasing = this.pointerInput.isEraseMode; const isErasing = this.pointerInput.isEraseMode;
const accentColor = const accentColor =
channelColors[runtimeSettings.selectedColorIndex] ?? channelColors[0]; channelColors[runtimeSettings.selectedColorIndex] ?? channelColors[0];

View file

@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest';
import { import {
ERASER_SIZE_MAX, ERASER_SIZE_MAX,
ERASER_SIZE_MIN, ERASER_SIZE_MIN,
getEffectiveEraserSize,
getEraserSizeFromSliderRatio, getEraserSizeFromSliderRatio,
getEraserSizeMaxForCssSize,
getEraserSliderRatioFromSize, getEraserSliderRatioFromSize,
} from './eraser-size-control'; } from '../config/eraser-size';
describe('eraser size slider mapping', () => { describe('eraser size slider mapping', () => {
it('maps slider position quadratically to eraser size', () => { it('maps slider position quadratically to eraser size', () => {
@ -23,4 +25,15 @@ describe('eraser size slider mapping', () => {
expect(getEraserSliderRatioFromSize(quarterRangeSize)).toBe(0.5); expect(getEraserSliderRatioFromSize(quarterRangeSize)).toBe(0.5);
expect(getEraserSliderRatioFromSize(ERASER_SIZE_MAX)).toBe(1); expect(getEraserSliderRatioFromSize(ERASER_SIZE_MAX)).toBe(1);
}); });
it('uses a responsive max size on small canvases', () => {
const mobileMax = getEraserSizeMaxForCssSize({ height: 640, width: 390 });
expect(mobileMax).toBeLessThan(ERASER_SIZE_MAX);
expect(getEraserSizeFromSliderRatio(1, mobileMax)).toBe(mobileMax);
expect(getEraserSliderRatioFromSize(ERASER_SIZE_MAX, mobileMax)).toBe(1);
expect(getEffectiveEraserSize(ERASER_SIZE_MAX, { height: 640, width: 390 })).toBe(
mobileMax
);
});
}); });

View file

@ -1,40 +1,25 @@
import {
clampEraserSize,
ERASER_SIZE_MAX,
getElementCssPixelSize,
getEraserSizeFromSliderRatio,
getEraserSizeMaxForCssSize,
getEraserSizeRatio,
getEraserSliderRatioFromSize,
} from '../config/eraser-size';
import type GameLoop from '../game-loop/game-loop'; import type GameLoop from '../game-loop/game-loop';
import { DEFAULT_ERASER_SIZE, settings } from '../settings'; import { DEFAULT_ERASER_SIZE, settings } from '../settings';
import { queryRequiredElement } from '../utils/dom'; import { queryRequiredElement } from '../utils/dom';
export const ERASER_SIZE_MIN = 24;
export const ERASER_SIZE_MAX = 480;
const ERASER_CONTROL_SCALE_MIN = 0.74; const ERASER_CONTROL_SCALE_MIN = 0.74;
const ERASER_CONTROL_SCALE_MAX = 1.34; const ERASER_CONTROL_SCALE_MAX = 1.34;
const clampEraserSize = (value: number): number => {
const safeValue = Number.isFinite(value) ? value : DEFAULT_ERASER_SIZE;
return Math.min(ERASER_SIZE_MAX, Math.max(ERASER_SIZE_MIN, Math.round(safeValue)));
};
const ERASER_SLIDER_MIN = 0; const ERASER_SLIDER_MIN = 0;
const ERASER_SLIDER_MAX = 1; const ERASER_SLIDER_MAX = 1;
const ERASER_SLIDER_STEP = 0.001; const ERASER_SLIDER_STEP = 0.001;
const clampSliderRatio = (value: number): number => { const clampStoredEraserSize = (value: number): number =>
const safeValue = Number.isFinite(value) ? value : ERASER_SLIDER_MIN; clampEraserSize(value, ERASER_SIZE_MAX, DEFAULT_ERASER_SIZE);
return Math.min(ERASER_SLIDER_MAX, Math.max(ERASER_SLIDER_MIN, safeValue));
};
const getEraserSizeRatio = (size: number): number => {
return (clampEraserSize(size) - ERASER_SIZE_MIN) / (ERASER_SIZE_MAX - ERASER_SIZE_MIN);
};
export const getEraserSizeFromSliderRatio = (sliderRatio: number): number => {
return clampEraserSize(
ERASER_SIZE_MIN +
(ERASER_SIZE_MAX - ERASER_SIZE_MIN) * clampSliderRatio(sliderRatio) ** 2
);
};
export const getEraserSliderRatioFromSize = (size: number): number =>
Math.sqrt(getEraserSizeRatio(size));
interface EraserSizeControlOptions { interface EraserSizeControlOptions {
getGame: () => GameLoop | null; getGame: () => GameLoop | null;
@ -48,6 +33,7 @@ export class EraserSizeControl {
HTMLLabelElement HTMLLabelElement
); );
private readonly slider = queryRequiredElement('.eraser-size-slider', HTMLInputElement); private readonly slider = queryRequiredElement('.eraser-size-slider', HTMLInputElement);
private readonly canvas = queryRequiredElement('canvas', HTMLCanvasElement);
private isActive = false; private isActive = false;
public constructor(private readonly options: EraserSizeControlOptions) { public constructor(private readonly options: EraserSizeControlOptions) {
@ -55,7 +41,10 @@ export class EraserSizeControl {
this.control.addEventListener('click', this.activate); this.control.addEventListener('click', this.activate);
this.slider.addEventListener('focus', this.activate); this.slider.addEventListener('focus', this.activate);
this.slider.addEventListener('input', () => { this.slider.addEventListener('input', () => {
settings.eraserSize = getEraserSizeFromSliderRatio(Number(this.slider.value)); settings.eraserSize = getEraserSizeFromSliderRatio(
Number(this.slider.value),
this.getResponsiveMaxSize()
);
this.activate(); this.activate();
this.render(); this.render();
this.options.onChange(); this.options.onChange();
@ -63,19 +52,21 @@ export class EraserSizeControl {
} }
public render(): void { public render(): void {
const size = clampEraserSize(settings.eraserSize); const maxSize = this.getResponsiveMaxSize();
if (settings.eraserSize !== size) { const storedSize = clampStoredEraserSize(settings.eraserSize);
settings.eraserSize = size; if (settings.eraserSize !== storedSize) {
settings.eraserSize = storedSize;
} }
const sliderRatio = getEraserSliderRatioFromSize(size); const size = clampEraserSize(storedSize, maxSize, DEFAULT_ERASER_SIZE);
const sliderRatio = getEraserSliderRatioFromSize(size, maxSize);
this.slider.min = ERASER_SLIDER_MIN.toString(); this.slider.min = ERASER_SLIDER_MIN.toString();
this.slider.max = ERASER_SLIDER_MAX.toString(); this.slider.max = ERASER_SLIDER_MAX.toString();
this.slider.step = ERASER_SLIDER_STEP.toString(); this.slider.step = ERASER_SLIDER_STEP.toString();
this.slider.value = sliderRatio.toString(); this.slider.value = sliderRatio.toString();
this.slider.setAttribute('aria-valuetext', `${size}px`); this.slider.setAttribute('aria-valuetext', `${size}px`);
const sizeRatio = getEraserSizeRatio(size); const sizeRatio = getEraserSizeRatio(size, maxSize);
const scale = const scale =
ERASER_CONTROL_SCALE_MIN + ERASER_CONTROL_SCALE_MIN +
(ERASER_CONTROL_SCALE_MAX - ERASER_CONTROL_SCALE_MIN) * sizeRatio; (ERASER_CONTROL_SCALE_MAX - ERASER_CONTROL_SCALE_MIN) * sizeRatio;
@ -95,6 +86,10 @@ export class EraserSizeControl {
this.options.onActivate(); this.options.onActivate();
}; };
private getResponsiveMaxSize(): number {
return getEraserSizeMaxForCssSize(getElementCssPixelSize(this.canvas));
}
private syncActiveState(): void { private syncActiveState(): void {
this.control.classList.toggle('active', this.isActive); this.control.classList.toggle('active', this.isActive);
this.slider.setAttribute( this.slider.setAttribute(

View file

@ -2,13 +2,10 @@
html > body > aside.control-dock > .info-page { html > body > aside.control-dock > .info-page {
width: min(100%, 520px); width: min(100%, 520px);
max-height: min(62vh, 480px); max-height: 200vh;
max-height: min(62dvh, 480px); max-height: 200dvh;
margin: 0 auto 10px; margin: 0 auto 10px;
overflow-x: hidden; overflow: hidden;
overflow-y: auto;
overscroll-behavior: contain;
touch-action: pan-y;
border: 1px solid rgb(255 255 255 / 46%); border: 1px solid rgb(255 255 255 / 46%);
border-radius: 8px; border-radius: 8px;
background: background:
@ -20,26 +17,12 @@ html > body > aside.control-dock > .info-page {
0 16px 42px rgb(0 0 0 / 30%), 0 16px 42px rgb(0 0 0 / 30%),
0 2px 10px rgb(0 0 0 / 18%); 0 2px 10px rgb(0 0 0 / 18%);
backdrop-filter: blur(16px) saturate(118%); backdrop-filter: blur(16px) saturate(118%);
scrollbar-width: thin;
scrollbar-color: rgb(69 98 88 / 62%) transparent;
-webkit-overflow-scrolling: touch;
transition: transition:
max-height var(--transition-time-long), max-height var(--transition-time-long),
opacity var(--transition-time-long), opacity var(--transition-time-long),
transform var(--transition-time-long), transform var(--transition-time-long),
margin-bottom var(--transition-time-long); margin-bottom var(--transition-time-long);
&::-webkit-scrollbar-track,
&::-webkit-scrollbar {
background-color: transparent;
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: rgb(69 98 88 / 62%);
border-radius: 8px;
}
&:focus-visible { &:focus-visible {
outline: 2px solid rgb(17 56 45); outline: 2px solid rgb(17 56 45);
outline-offset: 3px; outline-offset: 3px;
@ -225,8 +208,6 @@ html > body > aside.control-dock > .info-page {
@include on-small-screen { @include on-small-screen {
width: min(100%, 520px); width: min(100%, 520px);
max-height: min(58vh, 500px);
max-height: min(58dvh, 500px);
.info-page__content { .info-page__content {
gap: 0.75rem; gap: 0.75rem;
@ -244,26 +225,4 @@ html > body > aside.control-dock > .info-page {
line-height: 1.45; line-height: 1.45;
} }
} }
@media (max-height: 420px) {
max-height: min(
58vh,
max(
10rem,
calc(
100vh - 168px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)
)
)
);
max-height: min(
58dvh,
max(
10rem,
calc(
100dvh -
168px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)
)
)
);
}
} }

View file

@ -16,15 +16,24 @@
html { html {
height: 100%; height: 100%;
overscroll-behavior: none;
touch-action: manipulation; touch-action: manipulation;
user-select: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
body { body {
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
overscroll-behavior: none;
touch-action: manipulation; touch-action: manipulation;
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
-webkit-user-select: none;
} }
.visually-hidden { .visually-hidden {