Minor improvements
This commit is contained in:
parent
0fddad6b45
commit
4e24df1511
9 changed files with 175 additions and 100 deletions
12
index.html
12
index.html
|
|
@ -4,14 +4,14 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta
|
||||
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="robots" content="index,follow" />
|
||||
<meta name="author" content="Andras Schmelczer" />
|
||||
<meta
|
||||
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/" />
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
<meta property="og:locale" content="en_US" />
|
||||
<meta
|
||||
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:image" content="https://schmelczer.dev/fleeting/og-image.jpg" />
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
<meta name="twitter:title" content="Fleeting Garden" />
|
||||
<meta
|
||||
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:alt" content="Fleeting Garden social preview image." />
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
"@type": "WebApplication",
|
||||
"name": "Fleeting Garden",
|
||||
"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",
|
||||
"applicationCategory": "DesignApplication",
|
||||
"operatingSystem": "Any",
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
<div class="splash" data-visible="true">
|
||||
<h1 class="splash-title">Fleeting Garden</h1>
|
||||
<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>
|
||||
<button class="start-button" type="button" disabled>Start</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ const graphTuning = {
|
|||
latencyHint: 'interactive',
|
||||
outputFilterType: 'highpass',
|
||||
compressor: {
|
||||
thresholdDb: -18,
|
||||
kneeDb: 18,
|
||||
ratio: 2.1,
|
||||
attackSeconds: 0.018,
|
||||
thresholdDb: -22,
|
||||
kneeDb: 12,
|
||||
ratio: 4.5,
|
||||
attackSeconds: 0.006,
|
||||
releaseSeconds: 0.18,
|
||||
},
|
||||
} as const;
|
||||
|
|
@ -87,10 +87,12 @@ export class GardenAudioGraph {
|
|||
const context = new AudioContextConstructor({
|
||||
latencyHint: graphTuning.latencyHint,
|
||||
});
|
||||
const outputBus = context.createGain();
|
||||
const masterGain = context.createGain();
|
||||
const highPass = context.createBiquadFilter();
|
||||
const compressor = context.createDynamicsCompressor();
|
||||
|
||||
outputBus.gain.value = 1;
|
||||
masterGain.gain.value = 0;
|
||||
highPass.type = graphTuning.outputFilterType;
|
||||
highPass.frequency.value = outputHighPassFrequencyHz;
|
||||
|
|
@ -100,15 +102,17 @@ export class GardenAudioGraph {
|
|||
compressor.attack.value = graphTuning.compressor.attackSeconds;
|
||||
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);
|
||||
compressor.connect(context.destination);
|
||||
compressor.connect(masterGain);
|
||||
masterGain.connect(context.destination);
|
||||
|
||||
this.context = context;
|
||||
this.masterGain = masterGain;
|
||||
this.noiseBuffer = this.createNoiseBuffer(context);
|
||||
this.createDelay(context, masterGain);
|
||||
this.createBuses(context, masterGain);
|
||||
this.createDelay(context, outputBus);
|
||||
this.createBuses(context, outputBus);
|
||||
|
||||
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 delayNode = context.createDelay(graphTuning.delayMaxSeconds);
|
||||
const delayFeedback = context.createGain();
|
||||
|
|
@ -250,7 +254,7 @@ export class GardenAudioGraph {
|
|||
delayFeedback.connect(delayNode);
|
||||
delayNode.connect(returnLowPass);
|
||||
returnLowPass.connect(delayOutput);
|
||||
delayOutput.connect(masterGain);
|
||||
delayOutput.connect(outputBus);
|
||||
|
||||
this.delayInput = delayInput;
|
||||
this.delayNode = delayNode;
|
||||
|
|
@ -258,10 +262,10 @@ export class GardenAudioGraph {
|
|||
this.delayOutput = delayOutput;
|
||||
}
|
||||
|
||||
private createBuses(context: AudioContext, masterGain: GainNode): void {
|
||||
private createBuses(context: AudioContext, outputBus: GainNode): void {
|
||||
const eventBus = context.createGain();
|
||||
eventBus.gain.value = graphTuning.eventBusGain;
|
||||
eventBus.connect(masterGain);
|
||||
eventBus.connect(outputBus);
|
||||
this.eventBus = eventBus;
|
||||
this.pianoBuses.clear();
|
||||
|
||||
|
|
|
|||
84
src/config/eraser-size.ts
Normal file
84
src/config/eraser-size.ts
Normal 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));
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { getEffectiveEraserSize } from '../config/eraser-size';
|
||||
import { settings } from '../settings';
|
||||
|
||||
export class EraserPreview {
|
||||
|
|
@ -49,9 +50,15 @@ export class EraserPreview {
|
|||
};
|
||||
}
|
||||
|
||||
if (this.previousSize !== settings.eraserSize) {
|
||||
this.element.style.setProperty('--eraser-preview-size', `${settings.eraserSize}px`);
|
||||
this.previousSize = settings.eraserSize;
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const size = getEffectiveEraserSize(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 (
|
||||
|
|
@ -63,7 +70,6 @@ export class EraserPreview {
|
|||
return;
|
||||
}
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const left = `${this.previewClientPosition.x - rect.left}px`;
|
||||
const top = `${this.previewClientPosition.y - rect.top}px`;
|
||||
if (this.previousLeft !== left) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { vec2 } from 'gl-matrix';
|
|||
|
||||
import { GardenAudio } from '../audio/garden-audio';
|
||||
import { createGardenAudioConfig } from '../audio/garden-audio-config';
|
||||
import { getEffectiveEraserSize, getElementCssPixelSize } from '../config/eraser-size';
|
||||
import { activeVibe, settings } from '../settings';
|
||||
import { DeltaTimeCalculator } from '../utils/delta-time-calculator';
|
||||
import { rgbColorToCss, type RgbColor } from '../utils/rgb-color';
|
||||
|
|
@ -209,7 +210,11 @@ export default class GameLoop {
|
|||
const runtimeSettings = { ...settings };
|
||||
const introProgress = this.introPrompt.progress;
|
||||
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 accentColor =
|
||||
channelColors[runtimeSettings.selectedColorIndex] ?? channelColors[0];
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest';
|
|||
import {
|
||||
ERASER_SIZE_MAX,
|
||||
ERASER_SIZE_MIN,
|
||||
getEffectiveEraserSize,
|
||||
getEraserSizeFromSliderRatio,
|
||||
getEraserSizeMaxForCssSize,
|
||||
getEraserSliderRatioFromSize,
|
||||
} from './eraser-size-control';
|
||||
} from '../config/eraser-size';
|
||||
|
||||
describe('eraser size slider mapping', () => {
|
||||
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(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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 { DEFAULT_ERASER_SIZE, settings } from '../settings';
|
||||
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_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_MAX = 1;
|
||||
const ERASER_SLIDER_STEP = 0.001;
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
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));
|
||||
const clampStoredEraserSize = (value: number): number =>
|
||||
clampEraserSize(value, ERASER_SIZE_MAX, DEFAULT_ERASER_SIZE);
|
||||
|
||||
interface EraserSizeControlOptions {
|
||||
getGame: () => GameLoop | null;
|
||||
|
|
@ -48,6 +33,7 @@ export class EraserSizeControl {
|
|||
HTMLLabelElement
|
||||
);
|
||||
private readonly slider = queryRequiredElement('.eraser-size-slider', HTMLInputElement);
|
||||
private readonly canvas = queryRequiredElement('canvas', HTMLCanvasElement);
|
||||
private isActive = false;
|
||||
|
||||
public constructor(private readonly options: EraserSizeControlOptions) {
|
||||
|
|
@ -55,7 +41,10 @@ export class EraserSizeControl {
|
|||
this.control.addEventListener('click', this.activate);
|
||||
this.slider.addEventListener('focus', this.activate);
|
||||
this.slider.addEventListener('input', () => {
|
||||
settings.eraserSize = getEraserSizeFromSliderRatio(Number(this.slider.value));
|
||||
settings.eraserSize = getEraserSizeFromSliderRatio(
|
||||
Number(this.slider.value),
|
||||
this.getResponsiveMaxSize()
|
||||
);
|
||||
this.activate();
|
||||
this.render();
|
||||
this.options.onChange();
|
||||
|
|
@ -63,19 +52,21 @@ export class EraserSizeControl {
|
|||
}
|
||||
|
||||
public render(): void {
|
||||
const size = clampEraserSize(settings.eraserSize);
|
||||
if (settings.eraserSize !== size) {
|
||||
settings.eraserSize = size;
|
||||
const maxSize = this.getResponsiveMaxSize();
|
||||
const storedSize = clampStoredEraserSize(settings.eraserSize);
|
||||
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.max = ERASER_SLIDER_MAX.toString();
|
||||
this.slider.step = ERASER_SLIDER_STEP.toString();
|
||||
this.slider.value = sliderRatio.toString();
|
||||
this.slider.setAttribute('aria-valuetext', `${size}px`);
|
||||
|
||||
const sizeRatio = getEraserSizeRatio(size);
|
||||
const sizeRatio = getEraserSizeRatio(size, maxSize);
|
||||
const scale =
|
||||
ERASER_CONTROL_SCALE_MIN +
|
||||
(ERASER_CONTROL_SCALE_MAX - ERASER_CONTROL_SCALE_MIN) * sizeRatio;
|
||||
|
|
@ -95,6 +86,10 @@ export class EraserSizeControl {
|
|||
this.options.onActivate();
|
||||
};
|
||||
|
||||
private getResponsiveMaxSize(): number {
|
||||
return getEraserSizeMaxForCssSize(getElementCssPixelSize(this.canvas));
|
||||
}
|
||||
|
||||
private syncActiveState(): void {
|
||||
this.control.classList.toggle('active', this.isActive);
|
||||
this.slider.setAttribute(
|
||||
|
|
|
|||
|
|
@ -2,13 +2,10 @@
|
|||
|
||||
html > body > aside.control-dock > .info-page {
|
||||
width: min(100%, 520px);
|
||||
max-height: min(62vh, 480px);
|
||||
max-height: min(62dvh, 480px);
|
||||
max-height: 200vh;
|
||||
max-height: 200dvh;
|
||||
margin: 0 auto 10px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-y;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(255 255 255 / 46%);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
|
|
@ -20,26 +17,12 @@ html > body > aside.control-dock > .info-page {
|
|||
0 16px 42px rgb(0 0 0 / 30%),
|
||||
0 2px 10px rgb(0 0 0 / 18%);
|
||||
backdrop-filter: blur(16px) saturate(118%);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(69 98 88 / 62%) transparent;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
transition:
|
||||
max-height var(--transition-time-long),
|
||||
opacity var(--transition-time-long),
|
||||
transform 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 {
|
||||
outline: 2px solid rgb(17 56 45);
|
||||
outline-offset: 3px;
|
||||
|
|
@ -225,8 +208,6 @@ html > body > aside.control-dock > .info-page {
|
|||
|
||||
@include on-small-screen {
|
||||
width: min(100%, 520px);
|
||||
max-height: min(58vh, 500px);
|
||||
max-height: min(58dvh, 500px);
|
||||
|
||||
.info-page__content {
|
||||
gap: 0.75rem;
|
||||
|
|
@ -244,26 +225,4 @@ html > body > aside.control-dock > .info-page {
|
|||
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)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,15 +16,24 @@
|
|||
|
||||
html {
|
||||
height: 100%;
|
||||
overscroll-behavior: none;
|
||||
touch-action: manipulation;
|
||||
user-select: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
overscroll-behavior: none;
|
||||
touch-action: manipulation;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue