问题
I am trying to do some complicated effect, and to do it i have to break it down into its components, upon which i can build on and hopefully they will come together.
Now to make a circle in canvas is easy. But i want to make it myself. So I want to write a function that would be given a point that's center, radius, and then it will draw a circle with 1 px stroke width.
How would i go about it? If i look at from math perspective, what comes to mind is use circle distance formula and increment by small values, like .3 degrees, and make a dot at circumference. But if my circle is too small, like 2 px radius. Then it will waste lot of time drawing that won't matter, and if it's big enough you will see spaces between dots.
so i want my circle drawing function to draw a
- dot if radius is 1px.
- 4 dots around the center if radius is 2px.
- ..and so on.
- also if this gonna make my circle look rigid, i want there to be antialiasing too :D
I suppose once i know how to make outline filling it in won't be a problem..all i'd've to do is reduce the radius and keep drawing until radius is 1px.
回答1:
OK, I've re-factored my earlier code as a jQuery plugin named "canvasLens". It accepts a bunch of options to control things like image src, lens size and border color. You can even choose between two different lens effects, "fisheye" or "scaledSquare".
I've tried to make it as self-explanatory as possible with a header block and plenty of other comments.
/*
* Copyright (c) 2014 Roamer-1888
* "canvasLens"
* a jQuery plugin for a lens effect on one or more HTML5 canvases
* by Roamer-1888, 2014-11-09
* http://stackoverflow.com/users/3478010/roamer-1888
*
* Written in response to aa question by Muhammad Umer, here
* http://stackoverflow.com/questions/26793321/
*
* Invoke on a canvas element as follows
* $("#canvas").lens({
* imgSrc: 'path/to/image',
* imgCrossOrigin: '' | 'anonymous' | 'use-credentials', //[1]
* drawImageCoords: [ //[2]
* 0, 0, //(sx,st) Source image sub-rectangle Left,Top.
* 1350, 788, //(sw/sh) Source image sub-rectangle Width,Height.
* 0, 0, //(dx/dy) Destination Left,Top.
* 800, 467 //(dw/dh) Destination image sub-rectangle Width,Height.
* ],
* effect: 'fisheye' | 'scaledSquare',
* scale: 2 //currently affects only 'scaledSquare'
* size: 100, //diameter/side-length of the lens in pixels
* hideCursor: true | false,
* border: [0, 0, 0, 255] //[r,g,b,alpha] (base-10) | 'none'
* });
*
* Demo: http://jsfiddle.net/7z6by3o3/1/
*
* Further reading :
* [1] imgCrossOrigin -
* https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes
* [2] drawImageCoords -
* https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D
*
* Licence: MIT - http://en.wikipedia.org/wiki/MIT_License
*
* Please keep this header block intact, with amendments
* to reflect any changes made to the code.
*
*/
(function($){
// *******************************
// ***** Start: Private vars *****
// *******************************
var pluginName = 'canvasLens';
// *****************************
// ***** Fin: Private vars *****
// *****************************
// **********************************
// ***** Start: Private Methods *****
// **********************************
// Note that in all private methods,
// `this` is the canvas on which
// the plugin is invoked.
// Most private methods are called
// with `methodName.call(this)`.
// **********************************
function animate() {
var data = $(this).data(pluginName);
if(data) {
draw.call(this);
requestAnimationFrame(animate.bind(this));
}
}
function draw() {
var data = $(this).data(pluginName);
data.ctx.drawImage(data.m_can, 0, 0);
if(data.showLens) {
if(data.settings.effect == 'scaledSquare') {
scaledSquare.call(this);
} else {
fisheye.call(this);
}
}
}
function putBg() {
var data = $(this).data(pluginName);
data.m_ctx.drawImage.apply(data.m_ctx, [data.img].concat(data.settings.drawImageCoords));
}
function scaledSquare() {
var data = $(this).data(pluginName),
xt = data.settings.scale,
h = data.settings.size;
data.ctx.drawImage(data.m_can,
data.mouse.x - h/xt/2, data.mouse.y - h/xt/2, //sx,st Source image sub-rectangle Left,Top coordinates.
h/xt, h/xt, //sw/sh Source image sub-rectangle Width,Height.
data.mouse.x - h/2, data.mouse.y - h/2, //dx/dy Destination Left,Top coordinates.
h, h //dw/dh The Width,Height to draw the image in the destination canvas.
);
}
function fisheye() {
var data = $(this).data(pluginName),
d = data.settings.size,
mx = data.mouse.x, my = data.mouse.y,
srcpixels = data.m_ctx.getImageData(mx - d/2, my - d/2, d, d);
fisheyeTransform.call(this, srcpixels.data, data.xpixels.data, d, d);
data.ctx.putImageData(data.xpixels, mx - d/2, my - d/2);
}
function fisheyeTransform(srcData, xData, w, h) {
/*
* Fish eye effect (barrel distortion)
* *** adapted from ***
* tejopa, 2012-04-29
* http://popscan.blogspot.co.ke/2012/04/fisheye-lens-equation-simple-fisheye.html
*/
var data = $(this).data(pluginName),
y, x, ny, nx, ny2, nx2, r, nr, theta, nxn, nyn, x2, y2, pos, srcpos;
for (var y=0; y<h; y++) { // for each row
var ny = ((2 * y) / h) - 1; // normalize y coordinate to -1 ... 1
ny2 = ny * ny; // pre calculate ny*ny
for (x=0; x<w; x++) { // for each column
pos = 4 * (y * w + x);
nx = ((2 * x) / w) - 1; // normalize x coordinate to -1 ... 1
nx2 = nx * nx; // pre calculate nx*nx
r = Math.sqrt(nx2 + ny2); // calculate distance from center (0,0)
if(r > 1) {
/* 1-to-1 pixel mapping outside the circle */
/* An improvement would be to make this area transparent. ?How? */
xData[pos+0] = srcData[pos+0];//red
xData[pos+1] = srcData[pos+1];//green
xData[pos+2] = srcData[pos+2];//blue
xData[pos+3] = srcData[pos+3];//alpha
}
else if(data.settings.border && data.settings.border !== 'none' && r > (1-3/w) && r < 1) { // circular border around fisheye
xData[pos+0] = data.settings.border[0];//red
xData[pos+1] = data.settings.border[1];//green
xData[pos+2] = data.settings.border[2];//blue
xData[pos+3] = data.settings.border[3];//alpha
}
else if (0<=r && r<=1) { // we are inside the circle, let's do a fisheye transform on this pixel
nr = Math.sqrt(1 - Math.pow(r,2));
nr = (r + (1 - nr)) / 2; // new distance is between 0 ... 1
if (nr<=1) { // discard radius greater than 1.0
theta = Math.atan2(ny, nx); // calculate the angle for polar coordinates
nxn = nr * Math.cos(theta); // calculate new x position with new distance in same angle
nyn = nr * Math.sin(theta); // calculate new y position with new distance in same angle
x2 = Math.floor(((nxn + 1) * w) / 2); // map from -1 ... 1 to image coordinates
y2 = Math.floor(((nyn + 1) * h) / 2); // map from -1 ... 1 to image coordinates
srcpos = Math.floor(4 * (y2 * w + x2));
if (pos >= 0 && srcpos >= 0 && (pos+3) < xData.length && (srcpos+3) < srcData.length) { // make sure that position stays within arrays
/* get new pixel (x2,y2) and put it to target array at (x,y) */
xData[pos+0] = srcData[srcpos+0];//red
xData[pos+1] = srcData[srcpos+1];//green
xData[pos+2] = srcData[srcpos+2];//blue
xData[pos+3] = srcData[srcpos+3];//alpha
}
}
}
}
}
}
// ********************************
// ***** Fin: Private methods *****
// ********************************
// *********************************
// ***** Start: Public Methods *****
// *********************************
var methods = {
'init': function(options) {
//"this" is a jquery object on which this plugin has been invoked.
return this.each(function(index) {
var can = this,
$this = $(this);
var data = $this.data(pluginName);
if (!data) { // If the plugin hasn't been initialized yet
data = {
target: $this,
showLens: false,
mouse: {x:0, y:0}
};
$this.data(pluginName, data);
var settings = {
imgSrc: '',
imgCrossOrigin: '',
drawImageCoords: [
0, 0, //sx,st Source image sub-rectangle Left,Top coordinates.
500, 500, //sw/sh Source image sub-rectangle Width,Height.
0, 0, //dx/dy Destination Left,Top coordinates.
500, 500 //(dw/dh) Destination image sub-rectangle Width,Height.
],
effect: 'fisheye',
scale: 2,
size: 100,
border: [0, 0, 0, 255], //[r,g,b,alpha] base-10
hideCursor: false
};
if(options) {
$.extend(true, settings, options);
}
data.settings = settings;
if(settings.hideCursor) {
data.originalCursor = $this.css('cursor');
$this.css('cursor', 'none');
}
$this.on('mouseenter.'+pluginName, function(e) {
data.showLens = true;
}).on('mousemove.'+pluginName, function(e) {
data.mouse.x = e.offsetX;
data.mouse.y = e.offsetY;
}).on('mouseleave.'+pluginName, function(e) {
data.showLens = false;
});
data.m_can = $("<canvas>").attr({
'width': can.width,
'height': can.height
})[0];
data.ctx = can.getContext("2d"); // lens effect
data.m_ctx = data.m_can.getContext('2d'); // background image
data.xpixels = data.ctx.getImageData(0, 0, settings.size, settings.size);
data.img = new Image();
data.img.onload = function() {
putBg.call(can);
animate.call(can);
};
data.img.crossOrigin = settings.imgCrossOrigin;
data.img.src = settings.imgSrc;
}
});
},
'destroy': function() {
return this.each(function(index) {
var $this = $(this),
data = $this.data(pluginName);
$this.off('mouseenter.'+pluginName)
.off('mousemove.'+pluginName)
.off('mouseleave.'+pluginName);
if(data && data.originalCursor) {
$this.css('cursor', data.originalCursor);
}
$this.data(pluginName, null);
});
}
};
// *******************************
// ***** Fin: Public Methods *****
// *******************************
// *****************************
// ***** Start: Supervisor *****
// *****************************
$.fn[pluginName] = function( method ) {
if ( methods[method] ) {
return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || !method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist in jQuery.' + pluginName );
}
};
// ***************************
// ***** Fin: Supervisor *****
// ***************************
})(jQuery);
And here's a Demo.
Edit
Here's an attempt at explaining the fisheye (barrel distortion) calculations ...
- Starting with a blank lens of
w x hpixels. - The code loops through all pixels (target pixels).
- For each target pixel, chooses a pixel (source pixel) from the background image.
- The source pixel is always selected from those on (or close to) the same radial ray as the target, but at a smaller radial distance (using a formula for barrel distortion) from the lens's center.
- This is mechanised by calculation of the polar coordinates
(nr, theta)of the source pixel, then the application a standard math formula for converting polar back to rectangular coordinatesnxn = nr * Math.cos(theta)andnxn = nr * Math.sin(theta). Up to this point, all calculations have been made in normalised -1...0...1 space. - The rest of the code in the block denormaises (rescales), and (the bit I had to heavily adapt) actually implements the source to target pixel mapping by indexing into the 1-dimensional source and target data.
回答2:
You have the center x0, y0 and the radius r. Basically you need the parametric equation of circle:
x = x0 + r * cos(t)
y = y0 + r * sin(t)
Where t is the angle between a radial segment and normalized x-axis, and you need to divide it up as needed. For example for your four points case you do
360/4 = 90
and so use 0, 90, 180, 270 to get the four points.
来源:https://stackoverflow.com/questions/26793321/draw-a-circle-in-canvas-manually-pixel-by-pixel