Added UI for customisable block palettes

This commit is contained in:
Lucas Dower 2023-03-11 19:02:39 +00:00
parent e9c94a6d5a
commit 5853b335ce
No known key found for this signature in database
GPG Key ID: B3EE6B8499593605
13 changed files with 346 additions and 94 deletions

View File

@ -18,13 +18,13 @@ import { TexturedMaterialElement } from './ui/elements/textured_material_element
import { UI } from './ui/layout';
import { UIMessageBuilder } from './ui/misc';
import { ColourSpace, EAction } from './util';
import { ASSERT } from './util/error_util';
import { ASSERT, AppError } from './util/error_util';
import { download } from './util/file_util';
import { LOG_ERROR, Logger } from './util/log_util';
import { AppPaths } from './util/path_util';
import { Vector3 } from './vector';
import { TWorkerJob, WorkerController } from './worker_controller';
import { TFromWorkerMessage, TToWorkerMessage } from './worker_types';
import { download } from './util/file_util';
export class AppContext {
private _ui: UI;
@ -415,9 +415,15 @@ export class AppContext {
return { id: 'RenderNextVoxelMeshChunk', payload: payload, callback: callback };
}
private _assign(): TWorkerJob {
private _assign(): (TWorkerJob | undefined) {
const uiElements = this._ui.layout.assign.elements;
if (uiElements.blockPalette.getValue().count() <= 0) {
const outputElement = this._ui.getActionOutput(EAction.Assign);
outputElement.setTaskComplete('action', '[Block Mesh]: Failed', ['No blocks selected'], 'error');
return;
}
this._ui.getActionOutput(EAction.Assign)
.setTaskInProgress('action', '[Block Mesh]: Loading...');
@ -427,7 +433,7 @@ export class AppContext {
action: 'Assign',
params: {
textureAtlas: uiElements.textureAtlas.getValue(),
blockPalette: uiElements.blockPalette.getValue(),
blockPalette: uiElements.blockPalette.getValue().getBlocks(),
dithering: uiElements.dithering.getValue(),
colourSpace: ColourSpace.RGB,
fallable: uiElements.fallable.getValue() as FallableBehaviour,

View File

@ -100,7 +100,8 @@ export class BlockMesh {
ASSERT(atlas !== undefined, 'Could not load atlas');
this._atlas = atlas;
const palette = Palette.load(blockMeshParams.blockPalette);
const palette = Palette.create();
palette.add(blockMeshParams.blockPalette);
ASSERT(palette !== undefined, 'Could not load palette');
const atlasPalette = new AtlasPalette(atlas, palette);

View File

@ -11,7 +11,7 @@ export class AppConfig {
public readonly RELEASE_MODE = true;
public readonly MAJOR_VERSION = 0;
public readonly MINOR_VERSION = 7;
public readonly HOTFIX_VERSION = 11;
public readonly HOTFIX_VERSION = 12;
public readonly VERSION_TYPE: 'd' | 'a' | 'r' = 'r'; // dev, alpha, or release build
public readonly MINECRAFT_VERSION = '1.19.3';

View File

@ -26,10 +26,10 @@ export class Palette {
public static PALETTE_FILE_EXT: string = '.palette';
private static _FILE_VERSION: number = 1;
private _blocks: AppTypes.TNamespacedBlockName[];
private _blocks: Set<AppTypes.TNamespacedBlockName>;
private constructor() {
this._blocks = [];
this._blocks = new Set();
}
public static create(): Palette {
@ -55,7 +55,7 @@ export class Palette {
}
return outPalette;
return undefined;
/*
if (!Palette._isValidPaletteName(paletteName)) {
@ -116,31 +116,26 @@ export class Palette {
public add(blockNames: AppTypes.TNamespacedBlockName[]): void {
blockNames.forEach((blockName) => {
if (!this._blocks.includes(blockName)) {
this._blocks.push(AppUtil.Text.namespaceBlock(blockName));
if (!this._blocks.has(blockName)) {
this._blocks.add(AppUtil.Text.namespaceBlock(blockName));
}
});
}
public remove(blockName: string): boolean {
const index = this._blocks.indexOf(AppUtil.Text.namespaceBlock(blockName));
if (index !== -1) {
this._blocks.splice(index, 1);
return true;
}
return false;
return this._blocks.delete(blockName);
}
public has(blockName: string): boolean {
return this._blocks.includes(AppUtil.Text.namespaceBlock(blockName));
return this._blocks.has(AppUtil.Text.namespaceBlock(blockName));
}
public count() {
return this._blocks.length;
return this._blocks.size;
}
public getBlocks() {
return this._blocks;
return Array.from(this._blocks);
}
public static getAllPalette(): TOptional<Palette> {
@ -156,8 +151,9 @@ export class Palette {
*/
public removeMissingAtlasBlocks(atlas: Atlas) {
const missingBlocks: AppTypes.TNamespacedBlockName[] = [];
for (let blockIndex = this._blocks.length - 1; blockIndex >= 0; --blockIndex) {
const blockName = this._blocks[blockIndex];
const blocksCopy = Array.from(this._blocks);
for (const blockName of blocksCopy) {
if (!atlas.hasBlock(blockName)) {
missingBlocks.push(blockName);
this.remove(blockName);

View File

@ -18,11 +18,19 @@ export abstract class BaseUIElement<T> {
/**
* Get whether or not this UI element is interactable.
* @deprecated Use the enabled() getter.
*/
public getEnabled() {
return this._isEnabled;
}
/**
* Alias of `getEnabled`
*/
public get enabled() {
return this._isEnabled;
}
/**
* Set whether or not this UI element is interactable.
*/

View File

@ -4,11 +4,13 @@ import { ConfigUIElement } from './config_element';
export class CheckboxElement extends ConfigUIElement<boolean, HTMLSelectElement> {
private _labelChecked: string;
private _labelUnchecked: string;
private _hovering: boolean;
public constructor() {
super(false);
this._labelChecked = 'On';
this._labelUnchecked = 'Off';
this._hovering = false;
}
public setCheckedText(label: string) {
@ -23,30 +25,46 @@ export class CheckboxElement extends ConfigUIElement<boolean, HTMLSelectElement>
public override registerEvents(): void {
const checkboxElement = this._getElement();
const checkboxPipElement = UIUtil.getElementById(this._getPipId());
const textElement = UIUtil.getElementById(this._getTextId());
checkboxElement.addEventListener('mouseenter', () => {
if (this.getEnabled()) {
checkboxElement.classList.add('checkbox-hover');
checkboxPipElement.classList.add('checkbox-pip-hover');
}
this._onMouseEnterLeave(true);
});
checkboxElement.addEventListener('mouseleave', () => {
if (this.getEnabled()) {
checkboxElement.classList.remove('checkbox-hover');
checkboxPipElement.classList.remove('checkbox-pip-hover');
}
this._onMouseEnterLeave(false);
});
textElement.addEventListener('mouseenter', () => {
this._onMouseEnterLeave(true);
});
textElement.addEventListener('mouseleave', () => {
this._onMouseEnterLeave(false);
});
checkboxElement.addEventListener('click', () => {
if (this.getEnabled()) {
this._setValue(!this.getValue());
}
this._onClick();
});
textElement.addEventListener('click', () => {
this._onClick();
});
}
protected override _generateInnerHTML(): string {
private _onClick() {
if (this.enabled) {
this._setValue(!this.getValue());
}
}
private _onMouseEnterLeave(isHovering: boolean) {
this._hovering = isHovering;
this._updateStyles();
}
public override _generateInnerHTML(): string {
return `
<div class="checkbox" id="${this._getId()}">
<svg id="${this._getPipId()}" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
@ -59,36 +77,17 @@ export class CheckboxElement extends ConfigUIElement<boolean, HTMLSelectElement>
}
protected override _onValueChanged(): void {
const checkboxElement = this._getElement();
const checkboxPipElement = UIUtil.getElementById(this._getPipId());
const checkboxTextElement = UIUtil.getElementById(this._getTextId());
this._updateStyles();
}
checkboxTextElement.innerHTML = this.getValue() ? this._labelChecked : this._labelUnchecked;
checkboxPipElement.style.visibility = this.getValue() ? 'visible' : 'hidden';
if (this.getEnabled()) {
checkboxElement.classList.remove('checkbox-disabled');
} else {
checkboxElement.classList.add('checkbox-disabled');
}
public override finalise(): void {
this._onValueChanged();
}
protected override _onEnabledChanged(): void {
super._onEnabledChanged();
const checkboxElement = this._getElement();
const checkboxPipElement = UIUtil.getElementById(this._getPipId());
const checkboxTextElement = UIUtil.getElementById(this._getTextId());
if (this.getEnabled()) {
checkboxElement.classList.remove('checkbox-disabled');
checkboxTextElement.classList.remove('checkbox-text-disabled');
checkboxPipElement.classList.remove('checkbox-pip-disabled');
} else {
checkboxElement.classList.add('checkbox-disabled');
checkboxTextElement.classList.add('checkbox-text-disabled');
checkboxPipElement.classList.add('checkbox-pip-disabled');
}
this._updateStyles();
}
private _getPipId() {
@ -98,4 +97,45 @@ export class CheckboxElement extends ConfigUIElement<boolean, HTMLSelectElement>
private _getTextId() {
return this._getId() + '-label';
}
public check() {
this._setValue(true);
}
public uncheck() {
this._setValue(false);
}
private _updateStyles() {
const checkboxElement = UIUtil.getElementById(this._getId());
const checkboxPipElement = UIUtil.getElementById(this._getPipId());
const checkboxTextElement = UIUtil.getElementById(this._getTextId());
checkboxElement.classList.remove('checkbox-disabled');
checkboxElement.classList.remove('checkbox-hover');
checkboxPipElement.classList.remove('checkbox-pip-disabled');
checkboxPipElement.classList.remove('checkbox-pip-hover');
checkboxTextElement.classList.remove('text-dark');
checkboxTextElement.classList.remove('text-standard');
checkboxTextElement.classList.remove('text-light');
checkboxTextElement.classList.remove('checkbox-text-hover');
checkboxTextElement.innerHTML = this.getValue() ? this._labelChecked : this._labelUnchecked;
checkboxPipElement.style.visibility = this.getValue() ? 'visible' : 'hidden';
if (this.enabled) {
if (this._hovering) {
checkboxElement.classList.add('checkbox-hover');
checkboxPipElement.classList.add('checkbox-pip-hover');
checkboxTextElement.classList.add('text-light');
checkboxTextElement.classList.add('checkbox-text-hover');
} else if (this.getValue()) {
checkboxTextElement.classList.add('text-standard');
}
} else {
checkboxElement.classList.add('checkbox-disabled');
checkboxTextElement.classList.add('text-dark');
checkboxPipElement.classList.add('checkbox-pip-disabled');
}
}
}

View File

@ -7,7 +7,7 @@ import { BaseUIElement } from './base_element';
* For example, sliders, comboboxes and checkboxes are `ConfigUIElement`.
*/
export abstract class ConfigUIElement<T, F> extends BaseUIElement<F> {
private _label: string;
protected _label: string;
private _value?: T;
private _cachedValue?: T;
private _onValueChangedListeners: Array<(newValue: T) => void>;
@ -97,12 +97,12 @@ export abstract class ConfigUIElement<T, F> extends BaseUIElement<F> {
protected abstract _generateInnerHTML(): string;
protected override _onEnabledChanged() {
const label = UIUtil.getElementById(this._getLabelId()) as HTMLDivElement;
const label = document.getElementById(this._getLabelId()) as (HTMLDivElement | null);
if (this.getEnabled()) {
label.classList.remove('text-disabled');
label?.classList.remove('text-disabled');
} else {
label.classList.add('text-disabled');
label?.classList.add('text-disabled');
}
this._onEnabledChangedListeners.forEach((listener) => {
@ -127,7 +127,7 @@ export abstract class ConfigUIElement<T, F> extends BaseUIElement<F> {
*/
protected abstract _onValueChanged(): void;
private _getLabelId() {
protected _getLabelId() {
return this._getId() + '_label';
}
}

View File

@ -0,0 +1,18 @@
import { ConfigUIElement } from './config_element';
/**
* A `FullConfigUIElement` is a UI element that has a value the user can change.
* For example, sliders, comboboxes and checkboxes are `ConfigUIElement`.
*/
export abstract class FullConfigUIElement<T, F> extends ConfigUIElement<T, F> {
public override generateHTML() {
return `
<div class="property full-width-property" style="flex-direction: column; align-items: start;">
<div class="prop-key-container" id="${this._getLabelId()}">
${this._label}
</div>
${this._generateInnerHTML()}
</div>
`;
}
}

View File

@ -0,0 +1,140 @@
import { PALETTE_ALL_RELEASE } from '../../../res/palettes/all';
import { Palette } from '../../palette';
import { AppUtil } from '../../util';
import { UIUtil } from '../../util/ui_util';
import { ButtonElement } from './button';
import { CheckboxElement } from './checkbox';
import { FullConfigUIElement } from './full_config_element';
export class PaletteElement extends FullConfigUIElement<Palette, HTMLDivElement> {
private _checkboxes: { block: string, element: CheckboxElement }[];
private _palette: Palette;
private _selectAll: ButtonElement;
private _deselectAll: ButtonElement;
public constructor() {
super();
this._palette = Palette.create();
this._palette.add(PALETTE_ALL_RELEASE);
this._setValue(this._palette);
this._checkboxes = [];
PALETTE_ALL_RELEASE.forEach((block) => {
this._checkboxes.push({
block: block,
element: new CheckboxElement()
.setDefaultValue(true)
.setCheckedText(block)
.setUncheckedText(block),
});
});
this._selectAll = new ButtonElement()
.setLabel('Select All')
.setOnClick(() => {
this._checkboxes.forEach((checkbox) => {
checkbox.element.check();
});
});
this._deselectAll = new ButtonElement()
.setLabel('Deselect All')
.setOnClick(() => {
this._checkboxes.forEach((checkbox) => {
checkbox.element.uncheck();
});
});
}
protected override _generateInnerHTML(): string {
let checkboxesHTML = '';
this._checkboxes.forEach((checkbox) => {
checkboxesHTML += `<div class="col-container" id="${this._getId() + '-block-' + checkbox.block}">`;
checkboxesHTML += checkbox.element._generateInnerHTML();
checkboxesHTML += '</div>';
});
/*
<select>
<option value="All">All</option>
</select>
*/
return `
<div class="row-container" style="width: 100%; gap:">
<input type="text" style="width: 100%;" placeholder="Search..." id="${this._getId() + '-search'}"></input>
<div class="col-container" style="padding: 5px 0px;">
${this._selectAll.generateHTML()}
${this._deselectAll.generateHTML()}
</div>
<div class="row-container" style="border-radius: 5px; width: 100%; height: 200px; overflow-y: auto; overflow-x: hidden;" id="${this._getId() + '-list'}">
${checkboxesHTML}
</div>
</div>
`;
}
protected override _onValueChanged(): void {
}
protected override _onEnabledChanged(): void {
super._onEnabledChanged();
const searchElement = UIUtil.getElementById(this._getId() + '-search') as HTMLInputElement;
searchElement.disabled = !this.enabled;
this._checkboxes.forEach((checkbox) => {
checkbox.element.setEnabled(this.getEnabled());
});
this._selectAll.setEnabled(this.enabled);
this._deselectAll.setEnabled(this.enabled);
}
public override registerEvents(): void {
const searchElement = UIUtil.getElementById(this._getId() + '-search') as HTMLInputElement;
searchElement.addEventListener('keyup', () => {
this._onSearchBoxChanged(searchElement.value);
});
this._checkboxes.forEach((checkbox) => {
checkbox.element.registerEvents();
checkbox.element.addValueChangedListener(() => {
const isTicked = checkbox.element.getValue();
if (isTicked) {
this._palette.add([checkbox.block]);
console.log(this._palette.count());
} else {
this._palette.remove(checkbox.block);
console.log(this._palette.count());
}
});
});
this._selectAll.registerEvents();
this._deselectAll.registerEvents();
}
public override finalise(): void {
this._checkboxes.forEach((checkbox) => {
checkbox.element.finalise();
});
this._selectAll.finalise();
this._deselectAll.finalise();
}
private _onSearchBoxChanged(search: string) {
this._checkboxes.forEach((checkbox) => {
const row = UIUtil.getElementById(this._getId() + '-block-' + checkbox.block);
if (checkbox.block.toLocaleLowerCase().includes(search.toLowerCase()) || checkbox.block.toLowerCase().replace(/_/g, ' ').includes(search.toLocaleLowerCase())) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
}

View File

@ -20,6 +20,7 @@ import { ConfigUIElement } from './elements/config_element';
import { FileInputElement } from './elements/file_input';
import { HeaderUIElement } from './elements/header_element';
import { OutputElement } from './elements/output';
import { PaletteElement } from './elements/palette_element';
import { SliderElement } from './elements/slider';
import { ToolbarItemElement } from './elements/toolbar_item';
import { VectorSpinboxElement } from './elements/vector_spinbox';
@ -153,11 +154,7 @@ export class UI {
.addItems(this._getTextureAtlases())
.setLabel('Texture atlas')
.setShouldObeyGroupEnables(false),
'blockPalette': new ComboBoxElement<TPalettes>()
.addItem({ payload: 'all', displayText: 'All' })
.addItem({ payload: 'colourful', displayText: 'Colourful' })
.addItem({ payload: 'greyscale', displayText: 'Greyscale' })
.addItem({ payload: 'schematic-friendly', displayText: 'Schematic-friendly' })
'blockPalette': new PaletteElement()
.setLabel('Block palette'),
'dithering': new ComboBoxElement<TDithering>()
.addItems([{
@ -492,6 +489,7 @@ export class UI {
Split(['.column-properties', '.column-canvas'], {
sizes: [20, 80],
minSize: [400, 500],
snapOffset: 0,
});
const item = document.getElementsByClassName('gutter').item(0);

View File

@ -109,7 +109,7 @@ export type TPaletteId = string;
export namespace AssignParams {
export type Input = {
textureAtlas: TAtlasId,
blockPalette: TPalettes,
blockPalette: string[],
dithering: TDithering,
colourSpace: ColourSpace,
fallable: FallableBehaviour,

View File

@ -16,9 +16,10 @@
--prop-disabled: hsl(0, 0%, 14%);
--prop-sunken: hsl(0, 0%, 8%);
--text-light: hsl(0, 0%, 85%);
--text-standard: hsl(0, 0%, 66%);
--text-muted: hsl(0, 0%, 45%);
--text-disabled: hsl(0, 0%, 33%);
--text-dim: hsl(0, 0%, 45%);
--text-dark: hsl(0, 0%, 33%);
--vertical-divider: hsl(0, 0%, 14%);
@ -28,6 +29,11 @@
--subprop-value-width: 125px;
}
* {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box;
}
body {
margin: 0px;
@ -96,10 +102,16 @@ canvas {
align-items: center;
color: var(--text-standard);
font-size: 85%;
margin: 0px 0px;
padding: 5px 10px;
}
.full-width-property {
}
.full-width-property-container {
}
.big-padding {
padding-bottom: 25px;
}
@ -109,7 +121,7 @@ canvas {
.group-heading {
padding: 10px 10px;
color: #303030;
color: var(--text-dark);
font-weight: 400;
font-size: 85%;
letter-spacing: 4px;
@ -117,14 +129,16 @@ canvas {
.prop-key-container {
align-self: center;
display: flex;
align-items: center;
padding: 0px 10px 0px 0px;
width: 150px;
height: var(--property-height);
overflow: auto;
}
.text-disabled {
color: var(--text-disabled) !important;
color: var(--text-dark) !important;
}
.prop-value-container {
@ -191,7 +205,7 @@ canvas {
background: var(--prop-sunken);
box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 10px 0px inset;
border-radius: var(--border-radius);
color: #8C8C8C80;
color: var(--text-dark);
font-weight: 300;
font-size: 90%;
padding: 12px 18px;
@ -204,7 +218,7 @@ canvas {
}
.slider-height-normal {
height: calc(var(--property-height) - 4px);
height: var(--property-height);
}
.slider-height-small {
@ -213,8 +227,9 @@ canvas {
}
input {
padding: 0px;
margin: 0px;
-moz-appearance: none;
width: 20%;
font-size: 100%;
user-select: none;
margin-right: 3px;
@ -223,10 +238,13 @@ input {
background: var(--prop-standard);
font-family: 'Lexend', sans-serif;
font-weight: 300;
color: var(--text-standard);
outline-color: var(--prop-accent-hovered);
border: 1px solid rgb(255, 255, 255, 0.0);
}
input:enabled {
color: var(--text-standard);
}
input:hover:enabled {
color: #C6C6C6;
border: 1px solid rgb(255, 255, 255, 0.1);
@ -234,7 +252,7 @@ input:hover:enabled {
}
input:disabled {
background: var(--prop-disabled);
color: var(--text-disabled) !important;
color: var(--text-dark);
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
@ -244,6 +262,20 @@ input::-webkit-inner-spin-button {
input[type=number] {
-moz-appearance:textfield;
}
input[type=text] {
-moz-appearance:textfield;
height: var(--property-height);
text-align: start;
padding-left: 10px;
}
::placeholder {
color: var(--text-dark);
font-style: italic;
}
::placeholder:hover {
color: var(--text-standard) !important;
}
select {
font-size: 100%;
@ -270,11 +302,11 @@ select:hover:enabled {
}
select:disabled {
background: var(--prop-disabled) !important;
color: var(--text-disabled);
color: var(--text-dark);
}
.slider-bar-height-normal {
height: calc(var(--property-height) - 2px);
height: var(--property-height);
}
.slider-bar-height-small {
@ -287,7 +319,7 @@ select:disabled {
font-weight: 300;
background: var(--prop-standard);
cursor: ew-resize;
border: 1px solid var(--prop-bg);
border: 0px solid var(--prop-bg);
overflow: hidden;
flex-grow: 1;
}
@ -302,7 +334,7 @@ select:disabled {
.new-slider-bar {
border-radius: var(--border-radius);
height: calc(100% - 2px);
height: 100%;
background: var(--prop-accent-standard);
border: 1px solid rgb(255, 255, 255, 0.0);
}
@ -377,7 +409,7 @@ select:disabled {
}
.input-file-disabled {
background: var(--prop-disabled) !important;
color: var(--text-disabled) !important;
color: var(--text-dark) !important;
}
.h-div {
@ -470,7 +502,7 @@ select:disabled {
}
.spinbox-value-disabled {
color: var(--text-disabled);
color: var(--text-dark);
background: var(--prop-disabled) !important;
}
@ -585,7 +617,7 @@ svg {
}
.icon-disabled {
stroke: var(--text-disabled) !important;
stroke: var(--text-dark) !important;
}
.icon-disabled-active {
@ -783,10 +815,10 @@ svg {
.checkbox-text {
padding-left: 10px;
font-weight: 300;
color: var(--text-standard)
color: var(--text-dim)
}
.checkbox-text-disabled {
color: var(--text-disabled);
.checkbox-text-hover {
cursor: pointer;
}
.checkbox-pip {
@ -798,7 +830,7 @@ svg {
stroke: white;
}
.checkbox-pip-disabled {
stroke: var(--text-disabled);
stroke: var(--text-dark);
}
.spinbox-main-container {
@ -869,7 +901,7 @@ svg {
.subtitle {
font-size: 90%;
font-weight: 300;
color: var(--text-muted);
color: var(--text-dim);
}
.header-cols {
@ -899,4 +931,16 @@ svg {
a {
font-weight: 400;
color: var(--prop-accent-hovered)
}
.text-dark {
color: var(--text-dark);
}
.text-standard {
color: var(--text-standard);
}
.text-light {
color: var(--text-light);
}

View File

@ -1,5 +1,6 @@
import fs from 'fs';
import { PALETTE_ALL_RELEASE } from '../res/palettes/all';
import { ColourSpace } from '../src/util';
import { Vector3 } from '../src/vector';
import { THeadlessConfig } from './headless';
@ -20,7 +21,7 @@ export const headlessConfig: THeadlessConfig = {
},
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all', // Must be a palette name that exists in /resources/palettes
blockPalette: PALETTE_ALL_RELEASE, // Must be a palette name that exists in /resources/palettes
dithering: 'ordered',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',