konva/src/filters/Kaleidoscope.ts

273 lines
8.2 KiB
TypeScript
Raw Normal View History

2021-05-05 22:54:03 +08:00
import { Factory } from '../Factory';
2024-10-29 21:30:37 +08:00
import { Filter, Node } from '../Node';
2021-05-05 22:54:03 +08:00
import { Util } from '../Util';
import { getNumberValidator } from '../Validators';
2019-02-25 01:06:04 +08:00
2019-01-02 04:59:27 +08:00
/*
* ToPolar Filter. Converts image data to polar coordinates. Performs
* w*h*4 pixel reads and w*h pixel writes. The r axis is placed along
* what would be the y axis and the theta axis along the x axis.
* @function
* @author ippo615
* @memberof Konva.Filters
* @param {ImageData} src, the source image data (what will be transformed)
* @param {ImageData} dst, the destination image data (where it will be saved)
* @param {Object} opt
* @param {Number} [opt.polarCenterX] horizontal location for the center of the circle,
* default is in the middle
* @param {Number} [opt.polarCenterY] vertical location for the center of the circle,
* default is in the middle
*/
2024-10-05 22:18:32 +08:00
const ToPolar = function (src, dst, opt) {
2024-10-29 21:30:37 +08:00
const srcPixels = src.data,
2019-01-02 04:59:27 +08:00
dstPixels = dst.data,
xSize = src.width,
ySize = src.height,
xMid = opt.polarCenterX || xSize / 2,
2024-10-29 21:30:37 +08:00
yMid = opt.polarCenterY || ySize / 2;
2019-01-02 04:59:27 +08:00
// Find the largest radius
2024-10-29 21:30:37 +08:00
let rMax = Math.sqrt(xMid * xMid + yMid * yMid);
let x = xSize - xMid;
let y = ySize - yMid;
const rad = Math.sqrt(x * x + y * y);
2019-01-02 04:59:27 +08:00
rMax = rad > rMax ? rad : rMax;
// We'll be uisng y as the radius, and x as the angle (theta=t)
2024-10-29 21:30:37 +08:00
const rSize = ySize,
tSize = xSize;
2019-01-02 04:59:27 +08:00
// We want to cover all angles (0-360) and we need to convert to
// radians (*PI/180)
2024-10-29 21:30:37 +08:00
const conversion = ((360 / tSize) * Math.PI) / 180;
2019-01-02 04:59:27 +08:00
// var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale;
2024-10-29 21:30:37 +08:00
for (let theta = 0; theta < tSize; theta += 1) {
const sin = Math.sin(theta * conversion);
const cos = Math.cos(theta * conversion);
for (let radius = 0; radius < rSize; radius += 1) {
2019-01-02 04:59:27 +08:00
x = Math.floor(xMid + ((rMax * radius) / rSize) * cos);
y = Math.floor(yMid + ((rMax * radius) / rSize) * sin);
2024-10-29 21:30:37 +08:00
let i = (y * xSize + x) * 4;
const r = srcPixels[i + 0];
const g = srcPixels[i + 1];
const b = srcPixels[i + 2];
const a = srcPixels[i + 3];
2019-01-02 04:59:27 +08:00
// Store it
//i = (theta * xSize + radius) * 4;
i = (theta + radius * xSize) * 4;
dstPixels[i + 0] = r;
dstPixels[i + 1] = g;
dstPixels[i + 2] = b;
dstPixels[i + 3] = a;
}
}
};
/*
* FromPolar Filter. Converts image data from polar coordinates back to rectangular.
* Performs w*h*4 pixel reads and w*h pixel writes.
* @function
* @author ippo615
* @memberof Konva.Filters
* @param {ImageData} src, the source image data (what will be transformed)
* @param {ImageData} dst, the destination image data (where it will be saved)
* @param {Object} opt
* @param {Number} [opt.polarCenterX] horizontal location for the center of the circle,
* default is in the middle
* @param {Number} [opt.polarCenterY] vertical location for the center of the circle,
* default is in the middle
* @param {Number} [opt.polarRotation] amount to rotate the image counterclockwis,
* 0 is no rotation, 360 degrees is a full rotation
*/
2024-10-05 22:18:32 +08:00
const FromPolar = function (src, dst, opt) {
2024-10-29 21:30:37 +08:00
const srcPixels = src.data,
2019-01-02 04:59:27 +08:00
dstPixels = dst.data,
xSize = src.width,
ySize = src.height,
xMid = opt.polarCenterX || xSize / 2,
2024-10-29 21:30:37 +08:00
yMid = opt.polarCenterY || ySize / 2;
2019-01-02 04:59:27 +08:00
// Find the largest radius
2024-10-29 21:30:37 +08:00
let rMax = Math.sqrt(xMid * xMid + yMid * yMid);
let x = xSize - xMid;
let y = ySize - yMid;
const rad = Math.sqrt(x * x + y * y);
2019-01-02 04:59:27 +08:00
rMax = rad > rMax ? rad : rMax;
// We'll be uisng x as the radius, and y as the angle (theta=t)
2024-10-29 21:30:37 +08:00
const rSize = ySize,
2019-01-02 04:59:27 +08:00
tSize = xSize,
phaseShift = opt.polarRotation || 0;
// We need to convert to degrees and we need to make sure
// it's between (0-360)
// var conversion = tSize/360*180/Math.PI;
//var conversion = tSize/360*180/Math.PI;
2024-10-05 22:18:32 +08:00
let x1, y1;
2019-01-02 04:59:27 +08:00
for (x = 0; x < xSize; x += 1) {
for (y = 0; y < ySize; y += 1) {
2024-10-29 21:30:37 +08:00
const dx = x - xMid;
const dy = y - yMid;
const radius = (Math.sqrt(dx * dx + dy * dy) * rSize) / rMax;
let theta = ((Math.atan2(dy, dx) * 180) / Math.PI + 360 + phaseShift) % 360;
2019-01-02 04:59:27 +08:00
theta = (theta * tSize) / 360;
x1 = Math.floor(theta);
y1 = Math.floor(radius);
2024-10-29 21:30:37 +08:00
let i = (y1 * xSize + x1) * 4;
const r = srcPixels[i + 0];
const g = srcPixels[i + 1];
const b = srcPixels[i + 2];
const a = srcPixels[i + 3];
2019-01-02 04:59:27 +08:00
// Store it
i = (y * xSize + x) * 4;
dstPixels[i + 0] = r;
dstPixels[i + 1] = g;
dstPixels[i + 2] = b;
dstPixels[i + 3] = a;
}
}
};
//Konva.Filters.ToPolar = Util._FilterWrapDoubleBuffer(ToPolar);
//Konva.Filters.FromPolar = Util._FilterWrapDoubleBuffer(FromPolar);
// create a temporary canvas for working - shared between multiple calls
/*
* Kaleidoscope Filter.
* @function
* @name Kaleidoscope
* @author ippo615
* @memberof Konva.Filters
* @example
* node.cache();
* node.filters([Konva.Filters.Kaleidoscope]);
* node.kaleidoscopePower(3);
* node.kaleidoscopeAngle(45);
*/
2021-05-05 22:19:24 +08:00
export const Kaleidoscope: Filter = function (imageData) {
2024-10-05 22:18:32 +08:00
const xSize = imageData.width,
2019-01-02 04:59:27 +08:00
ySize = imageData.height;
2024-10-05 22:18:32 +08:00
let x, y, xoff, i, r, g, b, a, srcPos, dstPos;
let power = Math.round(this.kaleidoscopePower());
const angle = Math.round(this.kaleidoscopeAngle());
const offset = Math.floor((xSize * (angle % 360)) / 360);
2019-01-02 04:59:27 +08:00
if (power < 1) {
return;
}
// Work with our shared buffer canvas
2024-10-05 22:18:32 +08:00
const tempCanvas = Util.createCanvasElement();
2019-01-02 04:59:27 +08:00
tempCanvas.width = xSize;
tempCanvas.height = ySize;
2024-10-05 22:18:32 +08:00
const scratchData = tempCanvas
2023-08-28 22:23:57 +08:00
.getContext('2d')!
2019-01-02 04:59:27 +08:00
.getImageData(0, 0, xSize, ySize);
Util.releaseCanvas(tempCanvas);
2019-01-02 04:59:27 +08:00
// Convert thhe original to polar coordinates
ToPolar(imageData, scratchData, {
polarCenterX: xSize / 2,
2021-05-05 22:19:24 +08:00
polarCenterY: ySize / 2,
2019-01-02 04:59:27 +08:00
});
// Determine how big each section will be, if it's too small
// make it bigger
2024-10-05 22:18:32 +08:00
let minSectionSize = xSize / Math.pow(2, power);
2019-01-02 04:59:27 +08:00
while (minSectionSize <= 8) {
minSectionSize = minSectionSize * 2;
power -= 1;
}
minSectionSize = Math.ceil(minSectionSize);
2024-10-05 22:18:32 +08:00
let sectionSize = minSectionSize;
2019-01-02 04:59:27 +08:00
// Copy the offset region to 0
// Depending on the size of filter and location of the offset we may need
// to copy the section backwards to prevent it from rewriting itself
2024-10-05 22:18:32 +08:00
let xStart = 0,
2019-01-02 04:59:27 +08:00
xEnd = sectionSize,
xDelta = 1;
if (offset + minSectionSize > xSize) {
xStart = sectionSize;
xEnd = 0;
xDelta = -1;
}
for (y = 0; y < ySize; y += 1) {
for (x = xStart; x !== xEnd; x += xDelta) {
xoff = Math.round(x + offset) % xSize;
srcPos = (xSize * y + xoff) * 4;
r = scratchData.data[srcPos + 0];
g = scratchData.data[srcPos + 1];
b = scratchData.data[srcPos + 2];
a = scratchData.data[srcPos + 3];
dstPos = (xSize * y + x) * 4;
scratchData.data[dstPos + 0] = r;
scratchData.data[dstPos + 1] = g;
scratchData.data[dstPos + 2] = b;
scratchData.data[dstPos + 3] = a;
}
}
// Perform the actual effect
for (y = 0; y < ySize; y += 1) {
sectionSize = Math.floor(minSectionSize);
for (i = 0; i < power; i += 1) {
for (x = 0; x < sectionSize + 1; x += 1) {
srcPos = (xSize * y + x) * 4;
r = scratchData.data[srcPos + 0];
g = scratchData.data[srcPos + 1];
b = scratchData.data[srcPos + 2];
a = scratchData.data[srcPos + 3];
dstPos = (xSize * y + sectionSize * 2 - x - 1) * 4;
scratchData.data[dstPos + 0] = r;
scratchData.data[dstPos + 1] = g;
scratchData.data[dstPos + 2] = b;
scratchData.data[dstPos + 3] = a;
}
sectionSize *= 2;
}
}
// Convert back from polar coordinates
FromPolar(scratchData, imageData, { polarRotation: 0 });
};
/**
* get/set kaleidoscope power. Use with {@link Konva.Filters.Kaleidoscope} filter.
2019-01-06 16:01:20 +08:00
* @name Konva.Node#kaleidoscopePower
2019-01-02 04:59:27 +08:00
* @method
* @param {Integer} power of kaleidoscope
* @returns {Integer}
*/
Factory.addGetterSetter(
Node,
'kaleidoscopePower',
2,
2019-02-25 01:06:04 +08:00
getNumberValidator(),
2019-01-02 04:59:27 +08:00
Factory.afterSetFilter
);
/**
* get/set kaleidoscope angle. Use with {@link Konva.Filters.Kaleidoscope} filter.
2019-01-06 16:01:20 +08:00
* @name Konva.Node#kaleidoscopeAngle
2019-01-02 04:59:27 +08:00
* @method
* @param {Integer} degrees
* @returns {Integer}
*/
Factory.addGetterSetter(
Node,
'kaleidoscopeAngle',
0,
2019-02-25 01:06:04 +08:00
getNumberValidator(),
2019-01-02 04:59:27 +08:00
Factory.afterSetFilter
);