-
-
Notifications
You must be signed in to change notification settings - Fork 151
/
index.ts
154 lines (144 loc) · 4.67 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import type { TypedArray } from "@thi.ng/api";
import {
brightnessMat,
concat,
contrastMat,
exposureMat,
saturationMat,
temperatureMat,
transform,
} from "@thi.ng/color";
import { canvas, div, h3 } from "@thi.ng/hiccup-html";
import { FLOAT_RGBA, floatBufferFromImage, imageFromURL } from "@thi.ng/pixel";
import { $compile } from "@thi.ng/rdom";
import { compileForm, container, range } from "@thi.ng/rdom-forms";
import { reactive, sync, type ISubscription } from "@thi.ng/rstream";
import IMG from "./dolomites-960x940.jpg";
import { percent } from "@thi.ng/strings";
// image adjustment params
const saturation = reactive(1);
const contrast = reactive(1);
const brightness = reactive(0);
const exposure = reactive(1);
// color temperature (blue/yellow and green/magenta)
const tempBY = reactive(0);
const tempGM = reactive(0);
// preview A/B split position
const split = reactive(0);
// UI slider widget for a single image param
const ctrl = (
label: string,
value: ISubscription<number, number>,
min: number,
max: number
) =>
range({
label,
min,
max,
step: 0.01,
list: [(min + max) / 2], // create a tick mark for center position
value,
});
// iterator to segment a typed array buffer into vector views of `size`
function* mapBuffer(buf: TypedArray, size: number, stride = size) {
for (let i = 0; i < buf.length; i += stride) {
yield buf.subarray(i, i + size);
}
}
// extract local X position from mouse event and update split pos (for A/B comparison)
const setSplitPos = (e: MouseEvent) =>
split.next(
e.clientX - (<HTMLElement>e.target).getBoundingClientRect().left
);
// main app init
// create floating point pixel buffer from image
const srcImg = floatBufferFromImage(await imageFromURL(IMG), FLOAT_RGBA);
// create working copy
const destImg = srcImg.copy();
// pre-create vectors views for each pixel in both src & dest images
// this helps performance & avoids any other additional memory allocations
const srcPixels = [...mapBuffer(srcImg.data, 4)];
const destPixels = [...mapBuffer(destImg.data, 4)];
// create UI/DOM
await $compile(
div(
{},
h3(".mb3", {}, "Matrix-based image color adjustments"),
compileForm(
container(
{ class: "mb3" },
ctrl("exposure", exposure, 0, 2),
ctrl("brightness", brightness, -0.25, 0.25),
ctrl("contrast", contrast, 0, 2),
ctrl("saturation", saturation, 0, 2),
ctrl("temp (blue/yellow)", tempBY, -0.25, 0.25),
ctrl("temp (green/magenta)", tempGM, -0.25, 0.25)
),
{
labelAttribs: { class: "dib w5 v-top" },
typeAttribs: {
range: { class: "dib w4 w5-l" },
rangeLabel: { class: "ml3 v-top" },
rangeWrapper: { class: "dib" },
},
behaviors: {
rangeLabelFmt: percent(0),
},
}
),
canvas("#preview.pointer", {
width: srcImg.width,
height: srcImg.height,
title: "Click & drag to set split position",
onmousedown: setSplitPos,
onmousemove: (e) => {
if (e.buttons & 1) setSplitPos(e);
},
})
)
).mount(document.getElementById("app")!);
// keep reference to the preview canvas
const preview = <HTMLCanvasElement>document.getElementById("preview");
// initialize split display position to 50% (for A/B comparison)
split.next(srcImg.width >> 1);
// combine reactive image params, transform image whenever any param changes
// and return tuple of [src image, result image]
const images = sync({
src: { exposure, brightness, contrast, saturation, tempBY, tempGM },
}).map(({ exposure, brightness, contrast, saturation, tempBY, tempGM }) => {
// compose color transformation matrix from multiple adjustments
const mat = concat(
// color temperature (relative, 0.0 = original)
temperatureMat(tempBY, tempGM),
// saturation (absolute, 1.0 = original)
saturationMat(saturation),
// contrast (absolute, 1.0 = original)
contrastMat(contrast),
// brightness offset (relative, 0.0 = original)
brightnessMat(brightness),
// exposure/scale (absolute, 1.0 = original)
exposureMat(exposure)
);
// transform pixels using above matrix
for (let i = 0, n = srcPixels.length; i < n; i++) {
transform(destPixels[i], mat, srcPixels[i], false);
}
// return tuple of images
return [srcImg, destImg];
});
// combine transformed result with split pos and copy images to canvas (with
// A/B region split). we keep this as a separate processing step so that
// when only the split position is changed, we DON'T needlessly recompute
// the transformed image...
sync({ src: { images, split } }).subscribe({
next({ images: [src, dest], split }) {
// show transformed on the LHS
dest.getRegion(0, 0, split, dest.height)?.blitCanvas(preview);
// show original on the RHS
src.getRegion(split, 0, src.width - split, src.height)?.blitCanvas(
preview,
{ x: split }
);
},
});