25uint16_t i32tou16sat(int32_t i, int32_t cap = UINT16_MAX) {
26 return static_cast<uint16_t
>(std::clamp(i, 0, cap));
29uint16_t ftoabsu16(
float f) {
30 return i32tou16sat(
static_cast<int32_t
>(fabs(f)));
33using IndexFunction = std::function<int32_t(Vec2i, uint16_t, uint16_t, uint8_t, uint8_t)>;
35int32_t indexClamped(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) {
36 int32_t rX = i32tou16sat(v[0], w - 1);
37 int32_t rY = i32tou16sat(v[1], h - 1);
38 return (rY * w + rX) * pxLen + alphaOffs;
41int32_t indexReflected(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) {
42 int32_t rX = v[0] >= w ? w - v[0] % w : v[0];
43 int32_t rY = v[1] >= h ? h - v[1] % h : v[1];
44 return (rY * w + rX) * pxLen + alphaOffs;
47int32_t indexWrapped(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) {
48 int32_t rX = (v[0] + w) % w;
49 int32_t rY = (v[1] + h) % h;
50 return (rY * w + rX) * pxLen + alphaOffs;
59 return indexReflected;
84 , alphaOffs(alphaOffs)
89 , searchRadius(ftoabsu16(2.0f * std::max<float>(reduceX, reduceY) * distanceSpread))
91 , granularity(uint16_t{4} << ftoabsu16(ceil(log2(
static_cast<float>(this->searchRadius)))))
92 , offsX(sampleCentered ? reduceX / 2 : 0)
93 , offsY(sampleCentered ? reduceY / 2 : 0)
94 , alphaThreshold(alphaThreshold)
95 , indexFn(indexBy(edge))
98 float sample(Vec2i v)
const {
99 return this->srcImg[this->indexFn(v, this->imgWidth, this->imgHeight, this->pxLen, this->alphaOffs)];
102 Leaf thresh(
float alpha)
const {
103 return alpha >= this->alphaThreshold ? IN : OUT;
106 const float *
const srcImg;
107 const float alphaThreshold;
108 const uint16_t imgHeight;
109 const uint16_t imgWidth;
110 const uint16_t searchRadius;
111 const uint16_t granularity;
112 const uint16_t offsX, offsY;
113 const uint16_t reduceX, reduceY;
115 const uint8_t alphaOffs;
116 const IndexFunction indexFn;
121 QuadTree(
const SampleParam *param, uint32_t &totalComputation) : root(this->scanImg(0, 0, param->imgWidth, param->imgHeight, param, totalComputation))
128 using Branch = std::variant<Leaf, Node *>;
130 const std::array<const Branch, 4> branches;
131 Node(Branch nw, Branch ne, Branch sw, Branch se) : branches {nw, ne, sw, se} {};
133 for ( Branch branch : this->branches ) {
137 static void wither(Branch c) {
138 if (holds_alternative<Node *>(c)) {
139 delete get<Node*>(c);
144 static bool brancheq(Branch a, Branch b) {
145 return holds_alternative<Leaf>(a) && holds_alternative<Leaf>(b) && get<Leaf>(a) == get<Leaf>(b);
151 static Branch scanImg(uint16_t x, uint16_t y, uint16_t w, uint16_t h,
const SampleParam *param, uint32_t &totalComputation) {
152 if (w <= param->granularity || h <= param->granularity) {
154 int32_t ix = x - param->searchRadius + param->offsX, tx = x + w + param->searchRadius + param->offsX;
155 int32_t iy = y - param->searchRadius + param->offsY, ty = y + h + param->searchRadius + param->offsY;
156 Leaf curShade = param->thresh(param->sample({ix, iy}));
158 for (
auto jx = ix; jx < tx; jx++) {
159 for (
auto jy = iy; jy < ty; jy++) {
160 if (param->thresh(param->sample({jx, jy})) != curShade) {
161 totalComputation += w / param->reduceX * h / param->reduceY;
170 uint16_t hw = w / 2, hh = h / 2;
171 Branch iNW = scanImg(x, y, hw, hh, param, totalComputation);
172 Branch iNE = scanImg(x + hw, y, hw, hh, param, totalComputation);
173 Branch iSW = scanImg(x, y + hh, hw, hh, param, totalComputation);
174 Branch iSE = scanImg(x + hw, y + hh, hw, hh, param, totalComputation);
176 if (brancheq(iNW, iNE) && brancheq(iNE, iSW) && brancheq(iSW, iSE)) {
177 return get<Leaf>(iNW);
179 return new Node(iNW, iNE, iSW, iSE);
186 RasterCursor(
const QuadTree *tree, uint16_t imgWidth, uint16_t imgHeight, uint16_t reduceX = 1, uint16_t reduceY = 1, uint16_t offsX = 0, uint16_t offsY = 0)
189 , imgHeight(imgHeight)
196 this->curShade = this->reorient();
198 RasterCursor(
const QuadTree *tree,
const SampleParam *param)
199 : RasterCursor(tree, param->imgWidth, param->imgHeight, param->reduceX, param->reduceY, param->offsX, param->offsY)
202 bool getContiguousRun(Leaf &srcShade, uint16_t &srcY, uint16_t &srcX, uint16_t &dstW) {
206 srcShade = this->curShade = this->reorient();
208 while (this->curX < this->imgWidth && QuadTree::brancheq(srcShade, this->curShade)) {
209 auto blkSize = this->imgWidth >> this->depth;
210 this->curX += blkSize;
211 dstW += blkSize / this->reduceX;
212 this->curShade = this->reorient();
215 if (this->curX >= this->imgWidth) {
216 this->curX = this->offsX;
217 this->curY += this->reduceY;
220 return this->curY < this->imgHeight;
225 this->curBranch = this->tree->root;
226 auto remX = this->curX, remY = this->curY;
228 while (std::holds_alternative<QuadTree::Node *>(this->curBranch)) {
229 auto benchX = this->imgWidth >> (this->depth + 1), benchY = this->imgHeight >> (this->depth + 1);
230 if (benchX < 2 || benchY < 2) {
231 return this->curShade;
233 auto bGtX = remX >= benchX, bGtY = remY >= benchY;
234 remX -= benchX * bGtX;
235 remY -= benchY * bGtY;
236 this->curBranch = std::get<QuadTree::Node *>(this->curBranch)->branches[bGtX | bGtY << 1];
240 return get<Leaf>(this->curBranch);
243 const QuadTree *tree;
244 uint16_t curX, curY, depth;
245 const uint16_t imgWidth, imgHeight, reduceX, reduceY, offsX;
247 QuadTree::Branch curBranch;
250float vecDist(Vec2f32 v0, Vec2f32 v1) {
251 return std::hypot(v0[0] - v1[0], v1[0] - v1[1]);
253Vec2i vec2ipmul(Vec2i v0, Vec2i v1) {
254 return {v0[0] * v1[0], v0[1] * v1[1]};
260 const SampleParam *param,
263 uint8_t dstAlphaOffs,
266 const float *tangentDiffusion,
269 uint32_t totalComputation = 0;
271 const QuadTree tree(param, totalComputation);
272 RasterCursor cursor(&tree, param);
274 uint32_t iOut = dstAlphaOffs;
275 Leaf srcShade = FIGUREITOUT;
276 uint16_t srcY = 0, srcX = 0, dstW = 0;
278 float fRadius =
static_cast<float>(param->searchRadius);
279 uint16_t dstWidth = param->imgWidth / param->reduceX;
280 uint16_t dstHeight = param->imgHeight / param->reduceY;
283 std::vector<float> gradients = tangentDiffusion ? std::vector<float>(totalComputation, -pi_f32 - 0.05f) : std::vector<float>(0);
287 bool keepPainting =
true;
289 keepPainting = cursor.getContiguousRun(srcShade, srcY, srcX, dstW);
290 float fillDistance = 1.0f;
295 for (uint16_t i = 0; i < dstW; i++, iOut += dstPxLen) {
296 dstImg[iOut] = fillDistance;
302 for (uint16_t i = 0; i < dstW; i++, iGradW++, iOut += dstPxLen) {
303 int32_t curX = srcX + i * param->reduceX;
304 float alphaRef = param->sample({curX, srcY});
305 Leaf stateRef = param->thresh(alphaRef);
306 float nearest = std::numeric_limits<float>::max();
307 for (int32_t sX = curX - param->searchRadius, tX = curX + param->searchRadius; sX < tX; sX++) {
308 for (int32_t sY = srcY - param->searchRadius, tY = srcY + param->searchRadius; sY < tY; sY++) {
309 float alphaSmp = param->sample({sX, sY});
310 Leaf stateSmp = param->thresh(alphaSmp);
311 if (stateSmp != stateRef) {
312 float dist = std::hypot<float>(sX - curX, sY - srcY);
314 if (tangentDiffusion || distanceAA || euclidean) {
315 lastAng = atan2(sX - curX, sY - srcY);
317 if (distanceAA || euclidean) {
318 hypot1 = 1 / cos(fabs(fmod(lastAng + pi_f32 * 2.25f, pi_f32 / 2.0f) - pi_f32 / 4.0f));
321 dist += hypot1 * (1 - fabs(alphaSmp - alphaRef));
323 if (euclidean && dist > fRadius + hypot1) {
326 nearest = fmin(nearest, dist);
331 nearest = fmin(0.5f, nearest * 0.5f / fRadius);
332 if (stateRef == OUT) {
336 dstImg[iOut] = 0.5 + nearest;
337 if (tangentDiffusion) {
338 gradients[iGradW] = lastAng;
342 }
while (keepPainting);
344 if (tangentDiffusion) {
382 static constexpr std::array<const int8_t, 8> hardSigns{0, 1, 1, 1, 0, -1, -1, -1};
388 static constexpr float eighth = pi_f32 / 4.0f;
390 RasterCursor diffCursor(&tree, dstWidth, dstHeight);
391 uint32_t diffOut = dstAlphaOffs;
392 Leaf diffShade = FIGUREITOUT;
393 const uint32_t dstBufLen = dstWidth * dstHeight * dstPxLen;
394 uint16_t diffY = 0, diffX = 0, diffW = 0;
397 bool keepDiffusing =
true;
399 keepDiffusing = diffCursor.getContiguousRun(diffShade, diffY, diffX, diffW);
400 if (diffShade != FIGUREITOUT) {
403 bool rightEdge = diffX + diffW >= dstWidth;
405 for (uint16_t i = 0; i < diffW; i++, iGradR++, diffOut += dstPxLen) {
406 float theta = gradients[iGradR];
407 if (theta < -pi_f32 - 0.025f) {
410 float thetaNormal = fmod(theta + 2.5f * pi_f32, 2.0f * pi_f32);
411 uint8_t nEighths =
static_cast<uint32_t
>(std::floor(thetaNormal / eighth)) % 8;
412 double quantum = *tangentDiffusion;
413 double curPx = dstImg[diffOut];
414 double quantized = std::round(curPx / quantum) * quantum;
415 float quantErr = curPx - quantized;
416 dstImg[diffOut] = quantized;
417 bool atRight = rightEdge && i >= diffW - 1;
418 bool bottomEdge = diffY >= dstHeight - 1;
419 if (atRight && bottomEdge) {
422 Vec2f32 normal{cos(thetaNormal), sin(thetaNormal)};
424 float dist0 = vecDist(normal, {hardSigns[(nEighths + 2) % 8], hardSigns[(nEighths + 0) % 8]});
425 float dist1 = vecDist(normal, {hardSigns[(nEighths + 3) % 8], hardSigns[(nEighths + 1) % 8]});
427 Vec2i offs0{hardSigns[nEighths % 4 + 2], hardSigns[nEighths % 4 + 0]};
428 Vec2i offs1{hardSigns[nEighths % 4 + 3], hardSigns[nEighths % 4 + 1]};
429 Vec2i pos{diffX + i, diffY};
431 offs0 = vec2ipmul(offs0, {1, 0});
432 offs1 = vec2ipmul(offs1, {1, 0});
434 dstImg[param->indexFn(pos + offs0, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist1 / (dist0 + dist1);
435 dstImg[param->indexFn(pos + offs1, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist0 / (dist0 + dist1);
437 }
while (keepDiffusing);
441 *valveQuirks =
false;
443 auto blackOutAndWarn = [&](
auto oi) {
444 float& px = dstImg[oi * dstPxLen + dstAlphaOffs];
445 if (fabs(px) > 0.000001f) {
451 for (
auto x = 0; x < dstWidth; x++) {
453 blackOutAndWarn(x + (dstHeight - 1) * dstWidth);
455 for (
auto y = 0; y < dstHeight; y++) {
456 blackOutAndWarn(y * dstWidth);
457 blackOutAndWarn(y * dstWidth + dstWidth - 1);
462bool operator!(
Flags flags) {
468 return red(format) == bpp(format) && bpp(format);
474[[nodiscard]] std::vector<std::byte>
DistanceMapping::alphaToDistance(std::span<const std::byte> imageData,
ImageFormat inFormat,
ImageFormat outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY,
bool srgb,
float distanceSpread,
float alphaThreshold,
DistanceMapping::Flags flags,
DistanceMapping::Dither dither,
ImageConversion::ResizeFilter filter,
ImageConversion::ResizeEdge edge,
bool *valveQuirks) {
479 return isSingleChannel(format) ?
481 : (decompressedAlpha(format) || alpha(format)) ?
486 auto [intermediateRead, srcPxLen, srcAlphaOffs] = mkFormat(inFormat);
487 auto [intermediateWrite, dstPxLen, dstAlphaOffs] = mkFormat(outFormat);
494 || ftoabsu16(distanceSpread * reduceX) < 1
495 || ftoabsu16(distanceSpread * reduceY) < 1
496 || std::clamp(alphaThreshold, 0.0f, 1.0f) != alphaThreshold
502 bool inputNeedsConversion = intermediateRead != inFormat;
503 std::vector<std::byte> i_love_raii = inputNeedsConversion ?
convertImageDataToFormat(imageData, inFormat, intermediateRead, width, height) : std::vector<std::byte>(0);
504 std::span<const std::byte> srcImg = inputNeedsConversion ? i_love_raii : imageData;
506 SampleParam param(
reinterpret_cast<const float *
>(srcImg.data()), width, height, reduceX, reduceY, srcPxLen, srcAlphaOffs, distanceSpread, !!(flags &
Flags::SAMPLECENTERED), alphaThreshold, edge);
508 uint16_t dstWidth = width / reduceX, dstHeight = height / reduceY;
510 std::vector<std::byte> dstImg
511 = (!isSingleChannel(inFormat) && !isSingleChannel(outFormat)) ?
513 resizeImageData(imageData, inFormat, width, dstWidth, height, dstHeight, srgb,
true, filter, edge),
514 inFormat, intermediateWrite, dstWidth, dstHeight
517 std::vector<std::byte>(
sizeof(
float) * dstPxLen * dstWidth * dstHeight, std::byte{0x00});
519 float quantum = 1.0f /
static_cast<float>(1 << decompressedAlpha(outFormat));
523 reinterpret_cast<float *
>(dstImg.data()),
528 doGradientDither ? &quantum :
nullptr,
532 return (outFormat == intermediateWrite) ? dstImg :
convertImageDataToFormat(dstImg, intermediateWrite, outFormat, dstWidth, dstHeight);