Compare commits
25 Commits
dependabot
...
dev
Author | SHA1 | Date | |
---|---|---|---|
d87eff7645 | |||
f374068fb2 | |||
ecc715fe55 | |||
82caed4277 | |||
a7dff9475d | |||
d168f7a447 | |||
edf9cb755e | |||
a7503e69a2 | |||
5a9733563e | |||
2000e16ba2 | |||
7dbe0a7714 | |||
25bc43e409 | |||
cee51bf355 | |||
8d6daf0fc4 | |||
61209d0b62 | |||
d0b4855022 | |||
6cb64a59ca | |||
979fba0af1 | |||
b1df3e1d54 | |||
4f6138d97d | |||
6b6e3724d2 | |||
8ac5e6f678 | |||
a930e8d928 | |||
c814700cd2 | |||
dfdf2a7f71 |
42
codecs/qoi/Makefile
Normal file
42
codecs/qoi/Makefile
Normal file
@ -0,0 +1,42 @@
|
||||
CODEC_URL = https://github.com/phoboslab/qoi/archive/8d35d93cdca85d2868246c2a8a80a1e2c16ba2a8.tar.gz
|
||||
|
||||
CODEC_DIR = node_modules/qoi
|
||||
CODEC_BUILD_DIR:= $(CODEC_DIR)/build
|
||||
ENVIRONMENT = worker
|
||||
|
||||
OUT_JS = enc/qoi_enc.js dec/qoi_dec.js
|
||||
OUT_WASM := $(OUT_JS:.js=.wasm)
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(OUT_JS)
|
||||
|
||||
$(filter enc/%,$(OUT_JS)): enc/qoi_enc.o
|
||||
$(filter dec/%,$(OUT_JS)): dec/qoi_dec.o
|
||||
|
||||
# ALL .js FILES
|
||||
$(OUT_JS):
|
||||
$(LD) \
|
||||
$(LDFLAGS) \
|
||||
--bind \
|
||||
-s ENVIRONMENT=$(ENVIRONMENT) \
|
||||
-s EXPORT_ES6=1 \
|
||||
-o $@ \
|
||||
$+
|
||||
|
||||
# ALL .o FILES
|
||||
%.o: %.cpp $(CODEC_DIR)
|
||||
$(CXX) -c \
|
||||
$(CXXFLAGS) \
|
||||
-I $(CODEC_DIR) \
|
||||
-o $@ \
|
||||
$<
|
||||
|
||||
# CREATE DIRECTORY
|
||||
$(CODEC_DIR):
|
||||
mkdir -p $(CODEC_DIR)
|
||||
curl -sL $(CODEC_URL) | tar xz --strip 1 -C $(CODEC_DIR)
|
||||
|
||||
clean:
|
||||
$(RM) $(OUT_JS) $(OUT_WASM)
|
||||
$(MAKE) -C $(CODEC_DIR) clean
|
30
codecs/qoi/dec/qoi_dec.cpp
Normal file
30
codecs/qoi/dec/qoi_dec.cpp
Normal file
@ -0,0 +1,30 @@
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
|
||||
#define QOI_IMPLEMENTATION
|
||||
#include "qoi.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
|
||||
thread_local const val ImageData = val::global("ImageData");
|
||||
|
||||
val decode(std::string qoiimage) {
|
||||
qoi_desc desc;
|
||||
uint8_t* rgba = (uint8_t*)qoi_decode(qoiimage.c_str(), qoiimage.length(), &desc, 4);
|
||||
|
||||
// Resultant width and height stored in descriptor
|
||||
int decodedWidth = desc.width;
|
||||
int decodedHeight = desc.height;
|
||||
|
||||
val result = ImageData.new_(
|
||||
Uint8ClampedArray.new_(typed_memory_view(4 * decodedWidth * decodedHeight, rgba)),
|
||||
decodedWidth, decodedHeight);
|
||||
free(rgba);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
function("decode", &decode);
|
||||
}
|
7
codecs/qoi/dec/qoi_dec.d.ts
vendored
Normal file
7
codecs/qoi/dec/qoi_dec.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
export interface QOIModule extends EmscriptenWasm.Module {
|
||||
decode(data: BufferSource): ImageData | null;
|
||||
}
|
||||
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<QOIModule>;
|
||||
|
||||
export default moduleFactory;
|
16
codecs/qoi/dec/qoi_dec.js
generated
Normal file
16
codecs/qoi/dec/qoi_dec.js
generated
Normal file
File diff suppressed because one or more lines are too long
BIN
codecs/qoi/dec/qoi_dec.wasm
Normal file
BIN
codecs/qoi/dec/qoi_dec.wasm
Normal file
Binary file not shown.
36
codecs/qoi/enc/qoi_enc.cpp
Normal file
36
codecs/qoi/enc/qoi_enc.cpp
Normal file
@ -0,0 +1,36 @@
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
|
||||
#define QOI_IMPLEMENTATION
|
||||
#include "qoi.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
struct QoiOptions {};
|
||||
|
||||
thread_local const val Uint8Array = val::global("Uint8Array");
|
||||
|
||||
val encode(std::string buffer, int width, int height, QoiOptions options) {
|
||||
int compressedSizeInBytes;
|
||||
qoi_desc desc;
|
||||
desc.width = width;
|
||||
desc.height = height;
|
||||
desc.channels = 4;
|
||||
desc.colorspace = QOI_SRGB;
|
||||
|
||||
uint8_t* encodedData = (uint8_t*)qoi_encode(buffer.c_str(), &desc, &compressedSizeInBytes);
|
||||
if (encodedData == NULL)
|
||||
return val::null();
|
||||
|
||||
auto js_result =
|
||||
Uint8Array.new_(typed_memory_view(compressedSizeInBytes, (const uint8_t*)encodedData));
|
||||
free(encodedData);
|
||||
|
||||
return js_result;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
value_object<QoiOptions>("QoiOptions");
|
||||
|
||||
function("encode", &encode);
|
||||
}
|
14
codecs/qoi/enc/qoi_enc.d.ts
vendored
Normal file
14
codecs/qoi/enc/qoi_enc.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
export interface EncodeOptions {}
|
||||
|
||||
export interface QoiModule extends EmscriptenWasm.Module {
|
||||
encode(
|
||||
data: BufferSource,
|
||||
width: number,
|
||||
height: number,
|
||||
options: EncodeOptions,
|
||||
): Uint8Array;
|
||||
}
|
||||
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<QoiModule>;
|
||||
|
||||
export default moduleFactory;
|
16
codecs/qoi/enc/qoi_enc.js
generated
Normal file
16
codecs/qoi/enc/qoi_enc.js
generated
Normal file
File diff suppressed because one or more lines are too long
BIN
codecs/qoi/enc/qoi_enc.wasm
Normal file
BIN
codecs/qoi/enc/qoi_enc.wasm
Normal file
Binary file not shown.
7
codecs/qoi/package.json
Normal file
7
codecs/qoi/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "qoi",
|
||||
"scripts": {
|
||||
"build": "../build-cpp.sh"
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,26 @@
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
--size: 17px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background-color: var(--main-theme-color);
|
||||
border-radius: 999px;
|
||||
opacity: 0.25;
|
||||
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition-property: transform;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
&:focus-within::before {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.real-checkbox {
|
||||
|
@ -47,6 +47,10 @@ range-input::before {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
range-input:focus-within .thumb {
|
||||
outline: white solid 2px;
|
||||
}
|
||||
|
||||
.thumb-wrapper {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
|
@ -11,6 +11,10 @@
|
||||
padding: 3px calc(var(--thumb-size) / 2 + 3px);
|
||||
}
|
||||
|
||||
.checkbox:focus-within .track {
|
||||
outline: white solid 2px;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: relative;
|
||||
width: var(--thumb-size);
|
||||
|
@ -17,7 +17,7 @@ import Toggle from './Toggle';
|
||||
import Select from './Select';
|
||||
import { Options as QuantOptionsComponent } from 'features/processors/quantize/client';
|
||||
import { Options as ResizeOptionsComponent } from 'features/processors/resize/client';
|
||||
import { SwapIcon } from 'client/lazy-app/icons';
|
||||
import { ImportIcon, SaveIcon, SwapIcon } from 'client/lazy-app/icons';
|
||||
|
||||
interface Props {
|
||||
index: 0 | 1;
|
||||
@ -29,10 +29,14 @@ interface Props {
|
||||
onEncoderOptionsChange(index: 0 | 1, newOptions: EncoderOptions): void;
|
||||
onProcessorOptionsChange(index: 0 | 1, newOptions: ProcessorState): void;
|
||||
onCopyToOtherSideClick(index: 0 | 1): void;
|
||||
onSaveSideSettingsClick(index: 0 | 1): void;
|
||||
onImportSideSettingsClick(index: 0 | 1): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
supportedEncoderMap?: PartialButNotUndefined<typeof encoderMap>;
|
||||
leftSideSettings?: string | null;
|
||||
rightSideSettings?: string | null;
|
||||
}
|
||||
|
||||
type PartialButNotUndefined<T> = {
|
||||
@ -60,6 +64,8 @@ const supportedEncoderMapP: Promise<PartialButNotUndefined<typeof encoderMap>> =
|
||||
export default class Options extends Component<Props, State> {
|
||||
state: State = {
|
||||
supportedEncoderMap: undefined,
|
||||
leftSideSettings: localStorage.getItem('leftSideSettings'),
|
||||
rightSideSettings: localStorage.getItem('rightSideSettings'),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@ -69,6 +75,29 @@ export default class Options extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
private setLeftSideSettings = () => {
|
||||
this.setState({
|
||||
leftSideSettings: localStorage.getItem('leftSideSettings'),
|
||||
});
|
||||
};
|
||||
|
||||
private setRightSideSettings = () => {
|
||||
this.setState({
|
||||
rightSideSettings: localStorage.getItem('rightSideSettings'),
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
// Changing the state when side setting is stored in localstorage
|
||||
window.addEventListener('leftSideSettings', this.setLeftSideSettings);
|
||||
window.addEventListener('rightSideSettings', this.setRightSideSettings);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener('leftSideSettings', this.setLeftSideSettings);
|
||||
window.removeEventListener('removeSideSettings', this.setRightSideSettings);
|
||||
}
|
||||
|
||||
private onEncoderTypeChange = (event: Event) => {
|
||||
const el = event.currentTarget as HTMLSelectElement;
|
||||
|
||||
@ -110,6 +139,14 @@ export default class Options extends Component<Props, State> {
|
||||
this.props.onCopyToOtherSideClick(this.props.index);
|
||||
};
|
||||
|
||||
private onSaveSideSettingClick = () => {
|
||||
this.props.onSaveSideSettingsClick(this.props.index);
|
||||
};
|
||||
|
||||
private onImportSideSettingsClick = () => {
|
||||
this.props.onImportSideSettingsClick(this.props.index);
|
||||
};
|
||||
|
||||
render(
|
||||
{ source, encoderState, processorState }: Props,
|
||||
{ supportedEncoderMap }: State,
|
||||
@ -139,6 +176,36 @@ export default class Options extends Component<Props, State> {
|
||||
>
|
||||
<SwapIcon />
|
||||
</button>
|
||||
<button
|
||||
class={style.saveButton}
|
||||
title="Save side settings"
|
||||
onClick={this.onSaveSideSettingClick}
|
||||
>
|
||||
<SaveIcon />
|
||||
</button>
|
||||
<button
|
||||
class={
|
||||
style.importButton +
|
||||
' ' +
|
||||
(!this.state.leftSideSettings && this.props.index === 0
|
||||
? style.buttonOpacity
|
||||
: '') +
|
||||
' ' +
|
||||
(!this.state.rightSideSettings && this.props.index === 1
|
||||
? style.buttonOpacity
|
||||
: '')
|
||||
}
|
||||
title="Import saved side settings"
|
||||
onClick={this.onImportSideSettingsClick}
|
||||
disabled={
|
||||
// Disabled if this side's settings haven't been saved
|
||||
(!this.state.leftSideSettings &&
|
||||
this.props.index === 0) ||
|
||||
(!this.state.rightSideSettings && this.props.index === 1)
|
||||
}
|
||||
>
|
||||
<ImportIcon />
|
||||
</button>
|
||||
</div>
|
||||
</h3>
|
||||
<label class={style.sectionEnabler}>
|
||||
@ -190,7 +257,9 @@ export default class Options extends Component<Props, State> {
|
||||
onChange={this.onEncoderTypeChange}
|
||||
large
|
||||
>
|
||||
<option value="identity">Original Image</option>
|
||||
<option value="identity">{`Original Image ${
|
||||
this.props.source ? `(${this.props.source.file.name})` : ''
|
||||
}`}</option>
|
||||
{Object.entries(supportedEncoderMap).map(([type, encoder]) => (
|
||||
<option value={type}>{encoder.meta.label}</option>
|
||||
))}
|
||||
|
@ -53,6 +53,16 @@
|
||||
composes: option-toggle;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1em;
|
||||
|
||||
border-top: 1px solid #fff4;
|
||||
|
||||
transition-property: background-color;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.option-reveal:focus-within,
|
||||
.option-reveal:hover {
|
||||
background-color: #fff2;
|
||||
}
|
||||
|
||||
.option-one-cell {
|
||||
@ -73,11 +83,11 @@
|
||||
}
|
||||
|
||||
.text-field {
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
background-color: var(--black);
|
||||
color: var(--white);
|
||||
font: inherit;
|
||||
border: none;
|
||||
padding: 6px 0 6px 10px;
|
||||
padding: 6px 6px 6px 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
@ -118,4 +128,31 @@
|
||||
svg {
|
||||
fill: var(--header-text-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: var(--header-text-color) solid 2px;
|
||||
outline-offset: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.save-button,
|
||||
.import-button {
|
||||
composes: title-button;
|
||||
|
||||
svg {
|
||||
stroke: var(--header-text-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: var(--header-text-color) solid 2px;
|
||||
outline-offset: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.button-opacity {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
@ -111,6 +111,9 @@ async function decodeImage(
|
||||
if (mimeType === 'image/webp2') {
|
||||
return await workerBridge.wp2Decode(signal, blob);
|
||||
}
|
||||
if (mimeType === 'image/qoi') {
|
||||
return await workerBridge.qoiDecode(signal, blob);
|
||||
}
|
||||
}
|
||||
// Otherwise fall through and try built-in decoding for a laugh.
|
||||
return await builtinDecode(signal, blob);
|
||||
@ -281,24 +284,35 @@ export default class Compress extends Component<Props, State> {
|
||||
source: undefined,
|
||||
loading: false,
|
||||
preprocessorState: defaultPreprocessorState,
|
||||
// Tasking catched side settings if available otherwise taking default settings
|
||||
sides: [
|
||||
{
|
||||
latestSettings: {
|
||||
processorState: defaultProcessorState,
|
||||
encoderState: undefined,
|
||||
},
|
||||
loading: false,
|
||||
},
|
||||
{
|
||||
latestSettings: {
|
||||
processorState: defaultProcessorState,
|
||||
encoderState: {
|
||||
type: 'mozJPEG',
|
||||
options: encoderMap.mozJPEG.meta.defaultOptions,
|
||||
localStorage.getItem('leftSideSettings')
|
||||
? {
|
||||
...JSON.parse(localStorage.getItem('leftSideSettings') as string),
|
||||
loading: false,
|
||||
}
|
||||
: {
|
||||
latestSettings: {
|
||||
processorState: defaultProcessorState,
|
||||
encoderState: undefined,
|
||||
},
|
||||
loading: false,
|
||||
},
|
||||
localStorage.getItem('rightSideSettings')
|
||||
? {
|
||||
...JSON.parse(localStorage.getItem('rightSideSettings') as string),
|
||||
loading: false,
|
||||
}
|
||||
: {
|
||||
latestSettings: {
|
||||
processorState: defaultProcessorState,
|
||||
encoderState: {
|
||||
type: 'mozJPEG',
|
||||
options: encoderMap.mozJPEG.meta.defaultOptions,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
},
|
||||
],
|
||||
mobileView: this.widthQuery.matches,
|
||||
};
|
||||
@ -428,6 +442,99 @@ export default class Compress extends Component<Props, State> {
|
||||
sides: cleanSet(this.state.sides, otherIndex, oldSettings),
|
||||
});
|
||||
};
|
||||
/**
|
||||
* This function saves encodedSettings and latestSettings of
|
||||
* particular side in browser local storage
|
||||
* @param index : (0|1)
|
||||
* @returns
|
||||
*/
|
||||
private onSaveSideSettingsClick = async (index: 0 | 1) => {
|
||||
if (index === 0) {
|
||||
const leftSideSettings = JSON.stringify({
|
||||
encodedSettings: this.state.sides[index].encodedSettings,
|
||||
latestSettings: this.state.sides[index].latestSettings,
|
||||
});
|
||||
localStorage.setItem('leftSideSettings', leftSideSettings);
|
||||
// Firing an event when we save side settings in localstorage
|
||||
window.dispatchEvent(new CustomEvent('leftSideSettings'));
|
||||
await this.props.showSnack('Left side settings saved', {
|
||||
timeout: 1500,
|
||||
actions: ['dismiss'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (index === 1) {
|
||||
const rightSideSettings = JSON.stringify({
|
||||
encodedSettings: this.state.sides[index].encodedSettings,
|
||||
latestSettings: this.state.sides[index].latestSettings,
|
||||
});
|
||||
localStorage.setItem('rightSideSettings', rightSideSettings);
|
||||
// Firing an event when we save side settings in localstorage
|
||||
window.dispatchEvent(new CustomEvent('rightSideSettings'));
|
||||
await this.props.showSnack('Right side settings saved', {
|
||||
timeout: 1500,
|
||||
actions: ['dismiss'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This function sets the side state with catched localstorage
|
||||
* value as per side index provided
|
||||
* @param index : (0|1)
|
||||
* @returns
|
||||
*/
|
||||
private onImportSideSettingsClick = async (index: 0 | 1) => {
|
||||
const leftSideSettingsString = localStorage.getItem('leftSideSettings');
|
||||
const rightSideSettingsString = localStorage.getItem('rightSideSettings');
|
||||
|
||||
if (index === 0 && leftSideSettingsString) {
|
||||
const oldLeftSideSettings = this.state.sides[index];
|
||||
const newLeftSideSettings = {
|
||||
...this.state.sides[index],
|
||||
...JSON.parse(leftSideSettingsString),
|
||||
};
|
||||
this.setState({
|
||||
sides: cleanSet(this.state.sides, index, newLeftSideSettings),
|
||||
});
|
||||
const result = await this.props.showSnack('Left side settings imported', {
|
||||
timeout: 3000,
|
||||
actions: ['undo', 'dismiss'],
|
||||
});
|
||||
if (result === 'undo') {
|
||||
this.setState({
|
||||
sides: cleanSet(this.state.sides, index, oldLeftSideSettings),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (index === 1 && rightSideSettingsString) {
|
||||
const oldRightSideSettings = this.state.sides[index];
|
||||
const newRightSideSettings = {
|
||||
...this.state.sides[index],
|
||||
...JSON.parse(rightSideSettingsString),
|
||||
};
|
||||
this.setState({
|
||||
sides: cleanSet(this.state.sides, index, newRightSideSettings),
|
||||
});
|
||||
const result = await this.props.showSnack(
|
||||
'Right side settings imported',
|
||||
{
|
||||
timeout: 3000,
|
||||
actions: ['undo', 'dismiss'],
|
||||
},
|
||||
);
|
||||
if (result === 'undo') {
|
||||
this.setState({
|
||||
sides: cleanSet(this.state.sides, index, oldRightSideSettings),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
private onPreprocessorChange = async (
|
||||
preprocessorState: PreprocessorState,
|
||||
@ -829,6 +936,8 @@ export default class Compress extends Component<Props, State> {
|
||||
onEncoderOptionsChange={this.onEncoderOptionsChange}
|
||||
onProcessorOptionsChange={this.onProcessorOptionsChange}
|
||||
onCopyToOtherSideClick={this.onCopyToOtherClick}
|
||||
onSaveSideSettingsClick={this.onSaveSideSettingsClick}
|
||||
onImportSideSettingsClick={this.onImportSideSettingsClick}
|
||||
/>
|
||||
));
|
||||
|
||||
@ -842,7 +951,7 @@ export default class Compress extends Component<Props, State> {
|
||||
typeLabel={
|
||||
side.latestSettings.encoderState
|
||||
? encoderMap[side.latestSettings.encoderState.type].meta.label
|
||||
: 'Original Image'
|
||||
: `${side.file ? `${side.file.name}` : 'Original Image'}`
|
||||
}
|
||||
/>
|
||||
));
|
||||
|
@ -113,6 +113,13 @@
|
||||
|
||||
& > svg {
|
||||
width: 47px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&:focus .back-blob {
|
||||
stroke: var(--deep-blue);
|
||||
stroke-width: 5px;
|
||||
animation: strokePulse 500ms ease forwards;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
@ -124,6 +131,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes strokePulse {
|
||||
from {
|
||||
stroke-width: 8px;
|
||||
}
|
||||
to {
|
||||
stroke-width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.back-blob {
|
||||
fill: var(--hot-pink);
|
||||
opacity: 0.77;
|
||||
|
@ -97,3 +97,31 @@ export const SwapIcon = () => (
|
||||
<path d="M5.5 3.6v6.8L2.1 7l3.4-3.4M7 0L0 7l7 7V0zm4 0v14l7-7-7-7z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SaveIcon = () => (
|
||||
<svg viewBox="0 0 24 24">
|
||||
<g
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M12.501 20.93c-.866.25-1.914-.166-2.176-1.247a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.074.26 1.49 1.296 1.252 2.158M19 22v-6m3 3l-3-3l-3 3" />
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ImportIcon = () => (
|
||||
<svg viewBox="0 0 24 24">
|
||||
<g
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M12.52 20.924c-.87.262-1.93-.152-2.195-1.241a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.088.264 1.502 1.323 1.242 2.192M19 16v6m3-3l-3 3l-3-3" />
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
@ -103,6 +103,7 @@ const magicNumberMapInput = [
|
||||
[/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/, 'image/avif'],
|
||||
[/^\xff\x0a/, 'image/jxl'],
|
||||
[/^\x00\x00\x00\x0cJXL \x0d\x0a\x87\x0a/, 'image/jxl'],
|
||||
[/^qoif/, 'image/qoi'],
|
||||
] as const;
|
||||
|
||||
export type ImageMimeTypes = typeof magicNumberMapInput[number][1];
|
||||
|
19
src/features/decoders/qoi/worker/qoiDecode.ts
Normal file
19
src/features/decoders/qoi/worker/qoiDecode.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import qoiDecoder, { QOIModule } from 'codecs/qoi/dec/qoi_dec';
|
||||
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
|
||||
|
||||
let emscriptenModule: Promise<QOIModule>;
|
||||
|
||||
export default async function decode(blob: Blob): Promise<ImageData> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(qoiDecoder);
|
||||
}
|
||||
|
||||
const [module, data] = await Promise.all([
|
||||
emscriptenModule,
|
||||
blobToArrayBuffer(blob),
|
||||
]);
|
||||
|
||||
const result = module.decode(data);
|
||||
if (!result) throw new Error('Decoding error');
|
||||
return result;
|
||||
}
|
11
src/features/encoders/qoi/client/index.tsx
Normal file
11
src/features/encoders/qoi/client/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { EncodeOptions } from '../shared/meta';
|
||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
|
||||
export function encode(
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) {
|
||||
return workerBridge.qoiEncode(signal, imageData, options);
|
||||
}
|
13
src/features/encoders/qoi/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/qoi/client/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
19
src/features/encoders/qoi/shared/meta.ts
Normal file
19
src/features/encoders/qoi/shared/meta.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { EncodeOptions } from 'codecs/qoi/enc/qoi_enc';
|
||||
export { EncodeOptions };
|
||||
|
||||
export const label = 'QOI';
|
||||
export const mimeType = 'image/qoi';
|
||||
export const extension = 'qoi';
|
||||
export const defaultOptions: EncodeOptions = {};
|
13
src/features/encoders/qoi/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/qoi/shared/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
13
src/features/encoders/qoi/worker/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/qoi/worker/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
35
src/features/encoders/qoi/worker/qoiEncode.ts
Normal file
35
src/features/encoders/qoi/worker/qoiEncode.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import qoiEncoder, { QoiModule } from 'codecs/qoi/enc/qoi_enc';
|
||||
import type { EncodeOptions } from '../shared/meta';
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
|
||||
let emscriptenModule: Promise<QoiModule>;
|
||||
|
||||
async function init() {
|
||||
return initEmscriptenModule(qoiEncoder);
|
||||
}
|
||||
|
||||
export default async function encode(
|
||||
data: ImageData,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = init();
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const resultView = module.encode(data.data, data.width, data.height, options);
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return resultView.buffer as ArrayBuffer;
|
||||
}
|
@ -62,7 +62,7 @@ snack-bar {
|
||||
position: relative;
|
||||
flex: 0 1 auto;
|
||||
padding: 8px;
|
||||
height: 36px;
|
||||
height: 100%;
|
||||
margin: auto 8px auto -8px;
|
||||
min-width: 5em;
|
||||
background: none;
|
||||
@ -78,6 +78,7 @@ snack-bar {
|
||||
overflow: hidden;
|
||||
transition: background-color 200ms ease;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
|
@ -24,28 +24,28 @@ import SlideOnScroll from './SlideOnScroll';
|
||||
const demos = [
|
||||
{
|
||||
description: 'Large photo',
|
||||
size: '2.8mb',
|
||||
size: '2.8MB',
|
||||
filename: 'photo.jpg',
|
||||
url: largePhoto,
|
||||
iconUrl: largePhotoIcon,
|
||||
},
|
||||
{
|
||||
description: 'Artwork',
|
||||
size: '2.9mb',
|
||||
size: '2.9MB',
|
||||
filename: 'art.jpg',
|
||||
url: artwork,
|
||||
iconUrl: artworkIcon,
|
||||
},
|
||||
{
|
||||
description: 'Device screen',
|
||||
size: '1.6mb',
|
||||
size: '1.6MB',
|
||||
filename: 'pixel3.png',
|
||||
url: deviceScreen,
|
||||
iconUrl: deviceScreenIcon,
|
||||
},
|
||||
{
|
||||
description: 'SVG icon',
|
||||
size: '13k',
|
||||
size: '13KB',
|
||||
filename: 'squoosh.svg',
|
||||
url: logo,
|
||||
iconUrl: logoIcon,
|
||||
@ -319,7 +319,7 @@ export default class Intro extends Component<Props, State> {
|
||||
class="unbutton"
|
||||
onClick={(event) => this.onDemoClick(i, event)}
|
||||
>
|
||||
<div>
|
||||
<div class={style.demoContainer}>
|
||||
<div class={style.demoIconContainer}>
|
||||
<img
|
||||
class={style.demoIcon}
|
||||
|
@ -321,6 +321,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
transition: scale 400ms ease-in-out;
|
||||
&:hover {
|
||||
scale: 1.05;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-size {
|
||||
background: var(--dim-blue);
|
||||
border-radius: 1000px;
|
||||
|
@ -18,3 +18,14 @@ html {
|
||||
/* Old stuff: */
|
||||
--button-fg: rgb(95, 180, 228);
|
||||
}
|
||||
|
||||
@media (dynamic-range: high) {
|
||||
@supports (color: oklch(0% 0 0)) {
|
||||
html {
|
||||
--pink: oklch(75% 0.3 3);
|
||||
--hot-pink: oklch(65% 0.3 3);
|
||||
--blue: oklch(75% 0.3 248);
|
||||
--deep-blue: oklch(65% 0.3 248);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ import favicon from 'url:static-build/assets/favicon.ico';
|
||||
import ogImage from 'url:static-build/assets/icon-large-maskable.png';
|
||||
import { escapeStyleScriptContent, siteOrigin } from 'static-build/utils';
|
||||
import Intro from 'shared/prerendered-app/Intro';
|
||||
import snackbarCss from 'css:../../../shared/custom-els/snack-bar/styles.css';
|
||||
import * as snackbarStyle from '../../../shared/custom-els/snack-bar/styles.css';
|
||||
|
||||
interface Props {}
|
||||
|
||||
@ -73,6 +75,29 @@ const Index: FunctionalComponent<Props> = () => (
|
||||
<body>
|
||||
<div id="app">
|
||||
<Intro />
|
||||
<noscript>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: escapeStyleScriptContent(snackbarCss),
|
||||
}}
|
||||
/>
|
||||
<snack-bar>
|
||||
<div
|
||||
class={snackbarStyle.snackbar}
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
aria-hidden="false"
|
||||
>
|
||||
<div class={snackbarStyle.text}>
|
||||
Initialization error: This site requires JavaScript, which is
|
||||
disabled in your browser.
|
||||
</div>
|
||||
<a class={snackbarStyle.button} href="/">
|
||||
reload
|
||||
</a>
|
||||
</div>
|
||||
</snack-bar>
|
||||
</noscript>
|
||||
</div>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
|
Reference in New Issue
Block a user