Related
I've been trying to look over the Konva shape library and haven't found a stroke reapeating pattern method. I've been trying to look for a way to implement https://stackoverflow.com/a/32323610/20557085 into the shape's sceneFunc, but ended up with a static version that keeps itself in the top right corner of the canvas at all times, even if the canvas/camera is moved/dragged.
The end-goal would be to have a image that repeats itself following a line's bezier curve of points, that I can change the width of.
The question would be if there is something I am missing that is already a part of Konva, or if I should continue to trial my way through the sceneFunc?
The class component used in my attempt, that ended up static:
import React, { Component } from 'react';
import { createRoot } from 'react-dom/client';
import { Stage, Layer, Image, Shape } from 'react-konva';
var PI = Math.PI;
class URLImageStroke extends React.Component {
constructor(props) {
super(props)
this.state = {
image: null,
points: [{ x: 0, y: 0 }, { x: 100, y: 100 }, { x: 150, y: 50 }, { x: 200, y: 200 }]
};
}
componentDidMount() {
this.loadImage();
this.getPoints()
}
loadImage() {
// save to "this" to remove "load" handler on unmount
this.image = new window.Image();
this.image.src = this.props.src;
this.image.addEventListener('progress', (e) => console.log(e))
this.image.addEventListener('load', this.handleLoad);
}
handleLoad = () => {
this.setState({
image: this.image,
});
};
getPoints = () => {
let points = [];
//for (let i = 0; this.state.points.length > i; i++) {
const s = this.state.points[0];
const c1 = this.state.points[1];
const c2 = this.state.points[2];
const e = this.state.points[3];
for (var t = 0; t <= 100; t += 0.25) {
var T = t / 100;
// plot a point on the curve
var pos = getCubicBezierXYatT(s, c1, c2, e, T);
// calculate the tangent angle of the curve at that point
var tx = bezierTangent(s.x, c1.x, c2.x, e.x, T);
var ty = bezierTangent(s.y, c1.y, c2.y, e.y, T);
var a = Math.atan2(ty, tx) - PI / 2;
// save the x/y position of the point and the tangent angle
// in the points array
points.push({
x: pos.x,
y: pos.y,
angle: a
});
}
this.setState({
points: points
});
}
render() {
return (
<Shape
x={50}
y={50}
width={this.props?.width}
height={this.props?.height}
image={this.state.image}
points={this.state?.points}
sceneFunc={(ctx, shape) => {
const img = shape.attrs.image;
if (!img) {
console.log("no image")
return;
}
const points = shape.attrs.points;
if (!points) {
console.log("no points")
return;
}
// Note: increase the lineWidth if
// the gradient has noticable gaps
ctx.lineWidth = 8;
ctx.strokeStyle = 'skyblue';
let sliceCount = 0;
// draw a gradient-stroked line tangent to each point on the curve
for (let i = 0; i < points.length; i++) {
let p = points[i];
ctx.translate(p.x, p.y);
ctx.rotate(p.angle - PI / 2);
// draw multiple times to fill gaps on outside of rope slices
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
++sliceCount;
if (sliceCount > (img.width - 1)) { sliceCount = 0; }
}
//ctx.strokeShape(this);
}
}
/>
);
}
}
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
function getCubicBezierXYatT(startPt, controlPt1, controlPt2, endPt, T) {
var x = CubicN(T, startPt.x, controlPt1.x, controlPt2.x, endPt.x);
var y = CubicN(T, startPt.y, controlPt1.y, controlPt2.y, endPt.y);
return ({ x: x, y: y });
}
// cubic helper formula at T distance
function CubicN(T, a, b, c, d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T
+ (3 * b + T * (-6 * b + b * 3 * T)) * T
+ (c * 3 - c * 3 * T) * t2
+ d * t3;
}
// calculate the tangent angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
export default URLImageStroke;
I would like to transform histograms based on images to vector graphics.
This could be a start:
function preload() {
img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Cirrus_sky_panorama.jpg/1200px-Cirrus_sky_panorama.jpg");
}
function setup() {
createCanvas(400, 400);
background(255);
img.resize(0, 200);
var maxRange = 256
colorMode(HSL, maxRange);
image(img, 0, 0);
var histogram = new Array(maxRange);
for (i = 0; i <= maxRange; i++) {
histogram[i] = 0
}
loadPixels();
for (var x = 0; x < img.width; x += 5) {
for (var y = 0; y < img.height; y += 5) {
var loc = (x + y * img.width) * 4;
var h = pixels[loc];
var s = pixels[loc + 1];
var l = pixels[loc + 2];
var a = pixels[loc + 3];
b = int(l);
histogram[b]++
}
}
image(img, 0, 0);
stroke(300, 100, 80)
push()
translate(10, 0)
for (x = 0; x <= maxRange; x++) {
index = histogram[x];
y1 = int(map(index, 0, max(histogram), height, height - 300));
y2 = height
xPos = map(x, 0, maxRange, 0, width - 20)
line(xPos, y1, xPos, y2);
}
pop()
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.js"></script>
But I would need downloadable vector graphic files as results that are closed shapes without any gaps between. It should look like that for example:
<svg viewBox="0 0 399.84 200"><polygon points="399.84 200 399.84 192.01 361.91 192.01 361.91 182.87 356.24 182.87 356.24 183.81 350.58 183.81 350.58 184.74 344.91 184.74 344.91 188.19 339.87 188.19 339.87 189.89 334.6 189.89 334.6 185.29 328.93 185.29 328.93 171.11 323.26 171.11 323.26 172.55 317.59 172.55 317.59 173.99 311.92 173.99 311.92 179.42 306.88 179.42 306.88 182.03 301.21 182.03 301.21 183.01 295.54 183.01 295.54 179.04 289.87 179.04 289.87 175.67 284.21 175.67 284.21 182.03 278.54 182.03 278.54 176 273.5 176 273.5 172.42 267.83 172.42 267.83 179.42 262.79 179.42 262.79 182.03 257.12 182.03 257.12 183.01 251.45 183.01 251.45 178.63 245.78 178.63 245.78 175.21 240.11 175.21 240.11 182.03 234.86 182.03 234.86 150.42 229.2 150.42 229.2 155.98 223.53 155.98 223.53 158.06 217.86 158.06 217.86 167.44 212.19 167.44 212.19 162.58 206.52 162.58 206.52 155.98 200.85 155.98 200.85 158.06 195.18 158.06 195.18 167.44 189.51 167.44 189.51 177.46 183.84 177.46 183.84 166.93 178.17 166.93 178.17 153.69 172.5 153.69 172.5 155.87 166.82 155.87 166.82 158.05 161.78 158.05 161.78 155.63 156.11 155.63 156.11 160.65 150.84 160.65 150.84 146.59 145.17 146.59 145.17 109.63 139.49 109.63 139.49 113.67 133.82 113.67 133.82 61.48 128.15 61.48 128.15 80.59 123.11 80.59 123.11 93.23 117.44 93.23 117.44 97.97 111.76 97.97 111.76 78.07 106.09 78.07 106.09 61.66 100.42 61.66 100.42 93.23 94.75 93.23 94.75 98.51 89.7 98.51 89.7 85.4 84.03 85.4 84.03 111.03 78.99 111.03 78.99 120.57 73.32 120.57 73.32 124.14 67.65 124.14 67.65 23.48 61.97 23.48 61.97 0 56.3 0 56.3 120.57 50.63 120.57 50.63 167.01 45.38 167.01 45.38 170.83 39.71 170.83 39.71 172.26 34.03 172.26 34.03 178.7 28.36 178.7 28.36 175.36 22.69 175.36 22.69 170.83 17.02 170.83 17.02 172.26 11.34 172.26 11.34 178.7 5.67 178.7 5.67 103.85 0 103.85 0 200 399.84 200"/></svg>
Has anyone an idea how to program that? It doesn't necessarily need to be based on p5.js, but would be cool.
Closing Gaps
In order to have a gapless histogram, you need to meet the following condition:
numberOfBars * barWidth === totalWidth
Right now you are using the p5 line() function to draw your bars. You have not explicitly set the width of your bars, so it uses the default value of 1px wide.
We know that the numberOfBars in your code is always maxRange which is 256.
Right now the total width of your histogram is width - 20, where width is set to 400 by createCanvas(400, 400). So the totalWidth is 380.
256 * 1 !== 380
If you have 256 pixels of bars in a 380 pixel space then there are going to be gaps!
We need to change the barWidth and/or the totalWidth to balance the equation.
For example, you can change your canvas size to 276 (256 + your 20px margin) and the gaps disappear!
createCanvas(276, 400);
However this is not an appropriate solution because now your image is cropped and your pixel data is wrong. But actually...it was already wrong before!
Sampling Pixels
When you call the global loadPixels() function in p5.js you are loading all of the pixels for the whole canvas. This includes the white areas outside of your image.
for (var x = 0; x < img.width; x += 5) {
for (var y = 0; y < img.height; y += 5) {
var loc = (x + y * img.width) * 4;
It is a 1-dimensional array, so your approach of limiting the x and y values here is not giving you the correct position. Your loc variable needs to use the width of the entire canvas rather than the width of just the image, since the pixels array includes the entire canvas.
var loc = (x + y * width) * 4;
Alternatively, you can look at just the pixels of the image by using img.loadPixels() and img.pixels.
img.loadPixels();
for (var x = 0; x < img.width; x += 5) {
for (var y = 0; y < img.height; y += 5) {
var loc = (x + y * img.width) * 4;
var h = img.pixels[loc];
var s = img.pixels[loc + 1];
var l = img.pixels[loc + 2];
var a = img.pixels[loc + 3];
b = int(l);
histogram[b]++;
}
}
The pixel values are always returned in RGBA regardless of the colorMode. So your third channel value is actually the blue, not the lightness. You can make use of the p5.js lightness() function to compute the lightness from the RGBA.
Updated Code
The actual lightness histogram looks dumb because 100% dwarfs all of the other bars.
function preload() {
img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Cirrus_sky_panorama.jpg/1200px-Cirrus_sky_panorama.jpg");
}
function setup() {
const barCount = 100;
const imageHeight = 200;
createCanvas(400, 400);
background(255);
colorMode(HSL, barCount - 1);
img.resize(0, imageHeight);
imageMode(CENTER);
image(img, width / 2, imageHeight / 2);
img.loadPixels();
const histogram = new Array(barCount).fill(0);
for (let x = 0; x < img.width; x += 5) {
for (let y = 0; y < img.height; y += 5) {
const loc = (x + y * img.width) * 4;
const r = img.pixels[loc];
const g = img.pixels[loc + 1];
const b = img.pixels[loc + 2];
const a = img.pixels[loc + 3];
const barIndex = floor(lightness([r, g, b, a]));
histogram[barIndex]++;
}
}
fill(300, 100, 80);
strokeWeight(0);
const maxCount = max(histogram);
const barWidth = width / barCount;
const histogramHeight = height - imageHeight;
for (let i = 0; i < barCount; i++) {
const count = histogram[i];
const y1 = round(map(count, 0, maxCount, height, imageHeight));
const y2 = height;
const x1 = i * barWidth;
const x2 = x1 + barWidth;
rect(x1, y1, barWidth, height - y1);
}
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.js"></script>
But the blue channel histogram looks pretty good!
function preload() {
img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Cirrus_sky_panorama.jpg/1200px-Cirrus_sky_panorama.jpg");
}
function setup() {
const barCount = 100;
const imageHeight = 200;
createCanvas(400, 400);
background(255);
img.resize(0, imageHeight);
imageMode(CENTER);
image(img, width / 2, imageHeight / 2);
img.loadPixels();
const histogram = new Array(barCount).fill(0);
for (let x = 0; x < img.width; x += 5) {
for (let y = 0; y < img.height; y += 5) {
const loc = (x + y * img.width) * 4;
const r = img.pixels[loc];
const g = img.pixels[loc + 1];
const b = img.pixels[loc + 2];
const a = img.pixels[loc + 3];
const barIndex = floor(barCount * b / 255);
histogram[barIndex]++;
}
}
fill(100, 100, 300);
strokeWeight(0);
const maxCount = max(histogram);
const barWidth = width / barCount;
const histogramHeight = height - imageHeight;
for (let i = 0; i < barCount; i++) {
const count = histogram[i];
const y1 = round(map(count, 0, maxCount, height, imageHeight));
const y2 = height;
const x1 = i * barWidth;
const x2 = x1 + barWidth;
rect(x1, y1, barWidth, height - y1);
}
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.js"></script>
Just to add to Linda's excellent answer(+1), you can use p5.svg to render to SVG using p5.js:
let histogram;
function setup() {
createCanvas(660, 210, SVG);
background(255);
noStroke();
fill("#ed225d");
// make an array of 256 random values in the (0, 255) range
histogram = Array.from({length: 256}, () => int(random(255)));
//console.log(histogram);
// plot the histogram
barPlot(histogram, 0, 0, width, height);
// change shape rendering so bars appear connected
document.querySelector('g').setAttribute('shape-rendering','crispEdges');
// save the plot
save("histogram.svg");
}
function barPlot(values, x, y, plotWidth, plotHeight){
let numValues = values.length;
// calculate the width of each bar in the plot
let barWidth = plotWidth / numValues;
// calculate min/max value (to map height)
let minValue = min(values);
let maxValue = max(values);
// for each value
for(let i = 0 ; i < numValues; i++){
// map the value to the plot height
let barHeight = map(values[i], minValue, maxValue, 0, plotHeight);
// render each bar, offseting y
rect(x + (i * barWidth),
y + (plotHeight - barHeight),
barWidth, barHeight);
}
}
<script src="https://unpkg.com/p5#1.3.1/lib/p5.js"></script>
<script src="https://unpkg.com/p5.js-svg#1.0.7"></script>
(In the p5 editor (or when testing locally) a save dialog should pop up.
If you use the browser's Developer Tools to inspect the bar chart it should confirm it's an SVG (not <canvas/>))
I've map image (1816 x 8160) having following lat/lon of corners.
TopLeft: (-73.9308,40.8883)
TopRight: (-73.8584,40.858)
BottomLeft: (-74.0665,40.7024)
BottomRight: (-73.9944,40.6718)
Map is not true north and rotated at 28.34, also its UTM Zone 18N (78W to 72W). Here are further details about this map taken from PDF Maps iOS app.
Size (pixels): 1816 x 6160
Pixel Resolution: 3.829 meters
Bounds (pixels): (-1624, -3518) x (7866, 7719)
PROJCS["WGS 84 / UTM zone 18N",
GEOGCS["WGS 84",
DATUM["WGS_1984",
SPHEROID["WGS 84",6378137,298.257223563,
AUTHORITY["EPSG","7030"]],
TOWGS84[0,0,0,0,0,0,0],
AUTHORITY["EPSG","6326"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.0174532925199433,
AUTHORITY["EPSG","9122"]],
AUTHORITY["EPSG","4326"]],
PROJECTION["Transverse_Mercator"],
PARAMETER["latitude_of_origin",0],
PARAMETER["central_meridian",-75],
PARAMETER["scale_factor",0.9996],
PARAMETER["false_easting",500000],
PARAMETER["false_northing",0],
UNIT["metre",1,
AUTHORITY["EPSG","9001"]],
AXIS["Easting",EAST],
AXIS["Northing",NORTH],
AUTHORITY["EPSG","32618"]]
How to convert lat/lon to x y and vice versa?
Any help will be much appreciated.
Thanks in advance.
var dot_size = 15;
var longitude_shift = 0; //-28.34; // number of pixels your map's prime meridian is off-center.
var x_pos = 0; //54;
var y_pos = 0; //19;
var map_width = 1380; //1816; //430;
var map_height = 4682; //6160; //332;
var half_dot = Math.floor(dot_size / 2);
// Converts from degrees to radians.
Math.radians = function(degrees) {
return degrees * Math.PI / 180;
};
// Converts from radians to degrees.
Math.degrees = function(radians) {
return radians * 180 / Math.PI;
};
/* N 40.88839 -73.93071 //-73.9308
E 40.85789 -73.85843 //40.858 -73.8584
W 40.70228 -74.06652 //40.7024 -74.0665
S 40.67185 -73.99437 //40.6718 -73.9944 */
var bottomX = 40.67185;
var bottomY = -73.99437;
var topX = 40.88839; //-73.9308; //-73.9308,40.8883
var topY = -73.93071;
var degreesPerPixelX = (bottomX - topX) / map_width; //0.07225 / map_width;
var degreesPerPixelY = (bottomY - topY) / map_height; //0.18605/ map_height;
// These should roughly box Germany - use the actual values appropriate to your image
var minLat = bottomX;
var minLong = bottomY;
var maxLat = topX;
var maxLong = topY;
// Map image size (in points)
var mapSize = {'width': map_width, 'height': map_height};
// Determine the map scale (points per degree)
var xScale = mapSize.width / (maxLong - minLong);
var yScale = mapSize.height / (maxLat - minLat);
var south = Math.radians(40.67185); //lat 47.2
var north = Math.radians(40.88839); //lat 55.2
var west = Math.radians(-74.06652); //long 5.8
var east = Math.radians(-73.85843); //long 15.2
// Formula for mercator projection y coordinate:
function mercY(lat) { return Math.log(Math.tan(lat/2 + Math.PI/4)); }
// Some constants to relate chosen area to screen coordinates
var ymin = mercY(south);
var ymax = mercY(north);
var xFactor = mapSize.width/(east - west);
var yFactor = mapSize.height/(ymax - ymin);
var mapLonLeft = -74.06652; //9.8;
var mapLonRight = -73.85843; //10.2;
var mapLonDelta = mapLonRight - mapLonLeft;
mapLatBottom = 40.67185; //53.45;
mapLatBottomRadian = mapLatBottom * Math.PI / 180;
function convertGeoToPixel(lat, lon)
{
pX = (lon - mapLonLeft) * (mapSize.width / mapLonDelta);
lat1 = lat * Math.PI / 180;
worldMapWidth = ((mapSize.width / mapLonDelta) * 360) / (2 * Math.PI);
mapOffsetY = (worldMapWidth / 2 * Math.log((1 + Math.sin(mapLatBottomRadian)) / (1 - Math.sin(mapLatBottomRadian))));
pY = mapSize.height - ((worldMapWidth / 2 * Math.log((1 + Math.sin(lat1)) / (1 - Math.sin(lat1)))) - mapOffsetY);
return 'x:'+pX+', y:'+pY;
}
function convertPixelToGeo(tx, ty)
{
/* called worldMapWidth in Raphael's Code, but I think that's the radius since it's the map width or circumference divided by 2*PI */
var worldMapRadius = mapSize.width / mapLonDelta * 360/(2 * Math.PI);
var mapOffsetY = ( worldMapRadius / 2 * Math.log( (1 + Math.sin(mapLatBottomRadian) ) / (1 - Math.sin(mapLatBottomRadian)) ));
var equatorY = mapSize.height + mapOffsetY;
var a = (equatorY-ty)/worldMapRadius;
var lat = 180/Math.PI * (2 * Math.atan(Math.exp(a)) - Math.PI/2);
var long = mapLonLeft+tx/mapSize.width*mapLonDelta;
return 'lat:'+lat+', lng:'+long;
}
function draw_point(x, y) {
dot = '<div style="position:absolute;width:' + dot_size + 'px;height:' + dot_size + 'px;top:' + y + 'px;left:' + x + 'px;background:#00ff00"></div>';
document.body.innerHTML += dot;
}
function plot_point(lat, lng) {
spotLat = lat;
spotLong = lng;
// Mercator projection
// longitude: just scale and shift
x = (map_width * (180 + lng) / 360) % map_width + longitude_shift;
// latitude: using the Mercator projection
lat1 = lat * Math.PI / 180; // convert from degrees to radians
y = Math.log(Math.tan((lat1/2) + (Math.PI/4))); // do the Mercator projection (w/ equator of 2pi units)
y = (map_height / 2) - (map_width * y / (2 * Math.PI)) + y_pos; // fit it to our map
x -= x_pos;
y -= y_pos;
// position of map image for point
//var newXY = 'x:' (spotLong - minLong) * xScale + ', y:' + (spotLat - minLat) * yScale +'<br/>';
//var y = (spotLat - minLat) * yScale;
//alert('x: ' + kavraX(Math.radians(lat),Math.radians(lng)) + ', y: ' + kavraY(Math.radians(lat),Math.radians(lng)));
strText = 'kavra x:' + kavraX(Math.radians(lat),Math.radians(lng)) + ', y:' + kavraY(Math.radians(lat),Math.radians(lng)) + '<br/>';
strText += 'x:' + x + ', y:' + y + '<br/>';
strText += 'x:'+(spotLong - minLong) * xScale +', y:' + (spotLat - minLat) * yScale +'<br/>';
strText += 'x:'+((Math.radians(lng) - west)*xFactor)+' ,y:'+((ymax - mercY(Math.radians(lat)))*yFactor)+'<br/>';
strText += convertGeoToPixel(lat,lng)+'<br/>' ;
//floatingDiv = '<div style="position:fixed;top:10px;left:10px;">'+strText+'</div>';
//document.body.innerHTML += floatingDiv;
$('#leftDiv').html(strText);
draw_point(x - half_dot, y - half_dot);
}
function kavraX (latitude, longitude) // Kavra for Kavrayskiy
// formula from http://en.wikipedia.org/wiki/Kavrayskiy_VII_projection
{
return ((3 * longitude) / 2 * Math.PI)*Math.sqrt(Math.pow(Math.PI, 2)/3 - Math.pow(latitude, 2));
}
function kavraY (latitude, longitude)
{
return latitude*-1;
}
$(document).ready(function() {
//-73.949321, 40.796997
plot_point(40.764296, -73.973027);
$('img').click(function(e) {
var offset = $(this).offset();
var relativeX = (e.pageX - offset.left);
var relativeY = (e.pageY - offset.top);
var clickedLon = topX + relativeX * degreesPerPixelX;
var clickedLat = bottomY + relativeY * degreesPerPixelY;
alert(relativeX+':'+relativeY+' lat:'+clickedLat+", lon:"+clickedLon);
});
$('#parentDiv').mousemove(function(e) {
var offset = $('img').offset();
var relativeX = (e.pageX - offset.left);
var relativeY = (e.pageY - offset.top);
var clickedLat = topX + relativeX * degreesPerPixelX;
var clickedLon = topY + relativeY * degreesPerPixelY;
//alert(relativeX+':'+relativeY+' lat:'+clickedLat+", lon:"+clickedLon);
var strText = relativeX+':'+relativeY+' lat:'+clickedLat+", lon:"+clickedLon+'<br/>';
strText += 'lat:'+(relativeY / yScale + minLat)+', long:'+(relativeX / xScale + minLong)+'<br/>';
strText += convertPixelToGeo(relativeX,relativeY)+'<br/>';
//floatingDiv = '<div style="position:fixed;top:10px;right:10px;">'+strText+'</div>';
//document.body.innerHTML += floatingDiv;
$('#rightDiv').html(strText);
});
});
/*$(function() {
$("#test").click(function(e) {
var offset = $(this).offset();
var relativeX = (e.pageX - offset.left);
var relativeY = (e.pageY - offset.top);
alert(relativeX+':'+relativeY);
$(".position").val("afaf");
});
});*/
function onClick (evt) {
alert(evt.pageX +' '+ evt.pageY);
var x = getEventOffsetFromImageLeft(evt);
var y = getEventOffsetFromImageTop(evt);
var clickedLon = topX + x * degreesPerPixelX;
var clickedLat = bottomY + y * degreesPerPixelY;
}
</script>
</head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
<head>
</head>
<!-- onload="plot_point(40.756, -73.986)" -->
<body >
<div id='parentDiv'>
<!-- image found at http://i.stack.imgur.com/BXgSw.jpg -->
<img src="http://i.stack.imgur.com/BXgSw.jpg" style="position:absolute;top:0px;left:0px" >
<div id="leftDiv" style="position:fixed;top:10px;left:10px;"></div>
<div id="rightDiv" style="position:fixed;top:10px;right:10px;"></div>
</div>
</body>
</html>
I'm trying to find a rotated rectangle UIView's four corners' coordinates.
I think one way I can do is to use recognizer.rotation, find the rotated angle then calculate the origins. But that requires some geometry calculation.
- (IBAction)handlePan:(UIRotationGestureRecognizer*)recognizer {
NSLog(#"Rotation in degrees since last change: %f", [recognizer rotation] * (180 / M_PI));
recognizer.view.transform = CGAffineTransformRotate(recognizer.view.transform, recognizer.rotation);
NSLog(#"%#",recognizer);
recognizer.rotation = 0;
NSLog(#"bound is %f and %f, frame is %f and %f, %f and %f.",recognizer.view.bounds.size.width,recognizer.view.bounds.size.height, recognizer.view.frame.size.width,recognizer.view.frame.size.height, recognizer.view.frame.origin.x, recognizer.view.frame.origin.y);
}
I'm just wondering if there are any other easier ways to get the coordinates?
Thanks!
EDIT:
Looks like we have a great answer here(see answer below). I have managed to calculate the corners through a stupid way -- using rotation angle and geometry. It works but not easy and light. I'm sharing my code here just in case some one may want to use it(Even though I doubt it.)
float r = 100;
NSLog(#"radius is %f.",r);
float AAngle = M_PI/3+self.rotatedAngle;
float AY = recognizer.view.center.y - sin(AAngle)*r;
float AX = recognizer.view.center.x - cos(AAngle)*r;
self.pointPADA = CGPointMake(AX, AY);
NSLog(#"View Center is (%f,%f)",recognizer.view.center.x,recognizer.view.center.y);
NSLog(#"Point A has coordinate (%f,%f)",self.pointPADA.x,self.pointPADA.y);
float BAngle = M_PI/3-self.rotatedAngle;
float BY = recognizer.view.center.y - sin(BAngle)*r;
float BX = recognizer.view.center.x + cos(BAngle)*r;
self.pointPADB = CGPointMake(BX, BY);
NSLog(#"Point B has coordinate (%f,%f)",BX,BY);
float CY = recognizer.view.center.y + sin(AAngle)*r;
float CX = recognizer.view.center.x + cos(AAngle)*r;
self.pointPADC = CGPointMake(CX, CY);
NSLog(#"Point C has coordinate (%f,%f)",CX,CY);
float DY = recognizer.view.center.y + sin(BAngle)*r;
float DX = recognizer.view.center.x - cos(BAngle)*r;
self.pointPADD = CGPointMake(DX, DY);
NSLog(#"Point D has coordinate (%f,%f)",DX,DY);
Here's my solution though I wonder if there's a more succinct way:
CGPoint originalCenter = CGPointApplyAffineTransform(theView.center,
CGAffineTransformInvert(theView.transform));
CGPoint topLeft = originalCenter;
topLeft.x -= theView.bounds.size.width / 2;
topLeft.y -= theView.bounds.size.height / 2;
topLeft = CGPointApplyAffineTransform(topLeft, theView.transform);
CGPoint topRight = originalCenter;
topRight.x += theView.bounds.size.width / 2;
topRight.y -= theView.bounds.size.height / 2;
topRight = CGPointApplyAffineTransform(topRight, theView.transform);
CGPoint bottomLeft = originalCenter;
bottomLeft.x -= theView.bounds.size.width / 2;
bottomLeft.y += theView.bounds.size.height / 2;
bottomLeft = CGPointApplyAffineTransform(bottomLeft, theView.transform);
CGPoint bottomRight = originalCenter;
bottomRight.x += theView.bounds.size.width / 2;
bottomRight.y += theView.bounds.size.height / 2;
bottomRight = CGPointApplyAffineTransform(bottomRight, theView.transform);
Checked answer in Swift
struct ViewCorners {
private(set) var topLeft: CGPoint!
private(set) var topRight: CGPoint!
private(set) var bottomLeft: CGPoint!
private(set) var bottomRight: CGPoint!
private let originalCenter: CGPoint
private let transformedView: UIView
private func pointWith(multipliedWidth: CGFloat, multipliedHeight: CGFloat) -> CGPoint {
var x = originalCenter.x
x += transformedView.bounds.width / 2 * multipliedWidth
var y = originalCenter.y
y += transformedView.bounds.height / 2 * multipliedHeight
var result = CGPoint(x: x, y: y).applying(transformedView.transform)
result.x += transformedView.transform.tx
result.y += transformedView.transform.ty
return result
}
init(view: UIView) {
transformedView = view
originalCenter = view.center.applying(view.transform.inverted())
topLeft = pointWith(multipliedWidth:-1, multipliedHeight:-1)
topRight = pointWith(multipliedWidth: 1, multipliedHeight:-1)
bottomLeft = pointWith(multipliedWidth:-1, multipliedHeight: 1)
bottomRight = pointWith(multipliedWidth: 1, multipliedHeight: 1)
}
}
Then create struct instance and take transformed rect corners new points.
let view = UIView(frame: CGRect(x: 40, y: 20, width: 100, height: 50))
view.transform = .init(rotationAngle: .pi / 4)
let corners = ViewCorners(view: view)
print(corners.topLeft,
corners.topRight,
corners.bottomLeft,
corners.bottomRight,
separator: "\n")
Using Swift 4, We can get the bounds of rotated view by simple way.
let transformedBounds = view.bounds.applying(view.transform)
I am trying to programmatically create buttons for a interactive element that is part of my app. The interface looks like a doughnut chart, and if the user touches one of the regions in the doughnut chart, it will highlight that region and all of the others. I am not so concerned right now with the colors, I am trying to get the seven buttons to appear. Here is a code sample of how I am trying to create the buttons:
var radius = (Frame.Width - 38) / 2.0f;
var innerRadius = radius - 32;
var diameter = radius * 2;
energyCircleView = new UIView();
energyCircleView.BackgroundColor = UIColor.White;
energyCircleView.Frame = new RectangleF(19, confirmEntryButton.Frame.Top - 16 - (diameter), diameter, diameter);
energyCircleView.Layer.CornerRadius = radius;
energyCircleView.Layer.MasksToBounds = true;
var center = new PointF(energyCircleView.Frame.X + radius, energyCircleView.Frame.Y + radius);
var ninety = MathHelper.TwoPi * (90.0f / 360.0f);
var seventh = MathHelper.TwoPi * (1.0f / 7.0f) * 360.0f;
for (int i = 0; i < 7; i++)
{
var startAngle = ninety + (i * seventh);
var endAngle = ninety + ((i + 1) * seventh);
var shapePath = new CGPath();
shapePath.AddArc(center.X, center.Y, radius, startAngle, endAngle, false);
shapePath.AddArc(center.X, center.Y, innerRadius, endAngle, startAngle, true);
var shapeLayer = new CAShapeLayer();
shapeLayer.Path = shapePath;
var button = new UIButton();
button.Frame = new RectangleF(0, 0, diameter, diameter);
button.BackgroundColor = MyColors[i];
button.Layer.Mask = shapeLayer;
button.Layer.MasksToBounds = true;
energyButtons.Add(button);
}
foreach (UIButton button in energyButtons)
{
energyCircleView.AddSubview(button);
}
You are adding buttons as a subview of the view. But you do not show the view itself.
Add the following code to show energyCircleView:
this.View.AddSubview(energyCircleView);