Add files via upload
This commit is contained in:
628
sketch.js
Normal file
628
sketch.js
Normal file
@@ -0,0 +1,628 @@
|
||||
const pi = 3.14159265358979323846;
|
||||
let score = {
|
||||
strikes: 0,
|
||||
balls: 0,
|
||||
outs: 0,
|
||||
hits: 0
|
||||
}
|
||||
let midiSelectSlider;
|
||||
let canPitch, canSwing;
|
||||
let ball = {
|
||||
startX: 250, // 球開始座標
|
||||
startY: 50,
|
||||
angle: 0, // ball angle
|
||||
spinVel: 0.01, // ball spin velocity
|
||||
velX: 0, // ball velocity
|
||||
velY: 0,
|
||||
x: 250, // ball position
|
||||
y: 50,
|
||||
size: 50, // ball size
|
||||
speed: 0.05, // ball speed
|
||||
desiredX: 250, // ball desired position
|
||||
desiredXVel: 0,
|
||||
desiredY: 50,
|
||||
status: "ready",
|
||||
strike: false,
|
||||
hit: false,
|
||||
randomX: 0
|
||||
};
|
||||
let bat = {
|
||||
startAngle: pi * 1.5,
|
||||
endAngle: pi * -0.5,
|
||||
desiredAngle: pi * 1.5,
|
||||
angle: pi * 1.5,
|
||||
vel: 0,
|
||||
speed: 0.3,
|
||||
status: "ready",
|
||||
swung: false // 揮棒記錄
|
||||
};
|
||||
let pitchInput = {
|
||||
c: false,
|
||||
e: false,
|
||||
g: false,
|
||||
cTime: 0,
|
||||
eTime: 0,
|
||||
gTime: 0,
|
||||
cVel: 0,
|
||||
eVel: 0,
|
||||
gVel: 0
|
||||
};
|
||||
let pitcherDir = "C";
|
||||
let umpireReady = true;
|
||||
let messageOn = false;
|
||||
let messageType = "";
|
||||
|
||||
|
||||
function preload() {
|
||||
// images
|
||||
ballImg = loadImage('assets/ball.png');
|
||||
batImg = loadImage('assets/bat.png');
|
||||
plateImg = loadImage('assets/plate.png');
|
||||
|
||||
// SFX
|
||||
soundFormats('mp3');
|
||||
swishSFX = loadSound('assets/swish');
|
||||
swingSFX = loadSound('assets/swing');
|
||||
hitSFX = loadSound('assets/hit');
|
||||
catchSFX = loadSound('assets/catch');
|
||||
ballFourSFX = loadSound('assets/ballfour');
|
||||
fairBallSFX = loadSound('assets/fairball');
|
||||
outSFX = loadSound('assets/out');
|
||||
gameOverSFX = loadSound('assets/gameover');
|
||||
|
||||
|
||||
}
|
||||
|
||||
function setup() {
|
||||
let canvas = createCanvas(500, 800);
|
||||
getAudioContext().suspend();
|
||||
|
||||
canvas.parent('sketch-holder');
|
||||
colorMode(HSB, 100)
|
||||
frameRate(120);
|
||||
textAlign(CENTER, CENTER);
|
||||
// Init MIDI
|
||||
WebMidi.enable(function (err) { //check if WebMidi.js is enabled
|
||||
if (err) {
|
||||
console.log("WebMidi could not be enabled.", err);
|
||||
} else {
|
||||
console.log("WebMidi enabled!");
|
||||
}
|
||||
|
||||
//name our visible MIDI input and output ports
|
||||
console.log("---");
|
||||
console.log("Inputs Ports: ");
|
||||
for (i = 0; i < WebMidi.inputs.length; i++) {
|
||||
console.log(i + ": " + WebMidi.inputs[i].name);
|
||||
}
|
||||
|
||||
console.log("---");
|
||||
console.log("Output Ports: ");
|
||||
for (i = 0; i < WebMidi.outputs.length; i++) {
|
||||
console.log(i + ": " + WebMidi.outputs[i].name);
|
||||
}
|
||||
midiSelectSlider = select("#slider");
|
||||
midiSelectSlider.attribute("max", WebMidi.inputs.length - 1);
|
||||
midiSelectSlider.changed(inputChanged);
|
||||
midiIn = WebMidi.inputs[midiSelectSlider.value()]
|
||||
inputChanged();
|
||||
});
|
||||
resetGame();
|
||||
}
|
||||
|
||||
function draw() { // 主 Loop
|
||||
background(66, 20, 20);
|
||||
drawField();
|
||||
drawScore();
|
||||
updateBat();
|
||||
drawBat();
|
||||
calculatePitch();
|
||||
udpateBall()
|
||||
drawBall();
|
||||
checkHit();
|
||||
umpire();
|
||||
drawMessage();
|
||||
|
||||
// bat.angle -= 0.05;
|
||||
}
|
||||
|
||||
function checkHit() { // 檢查有沒有打到球
|
||||
textAlign(CENTER, CENTER)
|
||||
//let debugText = floor(mouseX) + "," + floor(mouseY);
|
||||
fill(255);
|
||||
stroke(255);
|
||||
//text(debugText, mouseX, mouseY - 10);
|
||||
let dist = 25;
|
||||
let len = 125;
|
||||
let ang = bat.angle - (pi * 0.25);
|
||||
let lineX1 = dist * cos(ang - 0.1) + 170;
|
||||
let lineX2 = (dist + len) * cos(ang - 0.1) + 170;
|
||||
let lineY1 = dist * sin(ang - 0.1) + 600;
|
||||
let lineY2 = (dist + len) * sin(ang - 0.1) + 600;
|
||||
let lineX3 = dist * cos(ang + 0.1) + 170;
|
||||
let lineX4 = (dist + len) * cos(ang + 0.1) + 170;
|
||||
let lineY3 = dist * sin(ang + 0.1) + 600;
|
||||
let lineY4 = (dist + len) * sin(ang + 0.1) + 600;
|
||||
//line(lineX1, lineY1, lineX2, lineY2); // bat visualizer
|
||||
// line(lineX3, lineY3, lineX4, lineY4);
|
||||
//let bottomText = "ball: " + ball.x + ", " + ball.y;
|
||||
//text(bottomText, 250, 750);
|
||||
// 用兩條線代表球棒(避免穿牆),檢查跟球是否重疊
|
||||
|
||||
if (lineCircle(lineX1, lineY1, lineX2, lineY2, ball.x, ball.y, ball.size / 2) || lineCircle(lineX3, lineY3, lineX4, lineY4, ball.x, ball.y, ball.size / 2)) {
|
||||
if (bat.angle < pi && bat.status == "swinging" && !ball.hit) {
|
||||
fill(100, 70, 100);
|
||||
ellipse(ball.x, ball.y, ball.size);
|
||||
// console.log("hit" + ball.x + ", " + ball.y + " " + bat.angle);
|
||||
ball.hit = true;
|
||||
|
||||
hitSFX.play();
|
||||
flyBall(ang, bat.vel, ball.x, ball.y, ball.speed); // ball is hit!
|
||||
console.log(ang, bat.vel, ball.x, ball.y, ball.speed);
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function umpire() { // 判斷好球壞球
|
||||
//stroke(255);
|
||||
//line(200, 600, 300, 600); // plate visualizer
|
||||
//line(200, 660, 300, 660);
|
||||
if (lineCircle(200, 600, 300, 600, ball.x, ball.y, ball.size / 2) || lineCircle(200, 660, 300, 660, ball.x, ball.y, ball.size / 2)) {
|
||||
console.log("strike zone passed.");
|
||||
ball.strike = true;
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function drawScore() {
|
||||
textFont('Verdana');
|
||||
textSize(80);
|
||||
fill(66, 20, 80, 20);
|
||||
noStroke();
|
||||
textAlign(RIGHT, TOP);
|
||||
textStyle(BOLD);
|
||||
// text(score.outs, 480, 20);
|
||||
text(score.hits, 130, 600);
|
||||
|
||||
let scoreText = "STRIKE " + "⚾️".repeat(score.strikes) + "\nBALLS " + "⚾️".repeat(score.balls) + "\nOUTS " + "⚾️".repeat(score.outs);
|
||||
textSize(18);
|
||||
fill(66, 10, 80);
|
||||
textAlign(LEFT, CENTER);
|
||||
textStyle(BOLD);
|
||||
text(scoreText, 300, 720);
|
||||
|
||||
|
||||
}
|
||||
|
||||
function drawMessage() {
|
||||
if (messageOn) {
|
||||
let type = messageType;
|
||||
let bigText, smallText = "";
|
||||
if (type == "hit") {
|
||||
bigText = "HIT!"
|
||||
smallText = ""
|
||||
}
|
||||
if (type == "foul") {
|
||||
bigText = "FOUL"
|
||||
smallText = ""
|
||||
}
|
||||
if (type == "strike") {
|
||||
bigText = "STRIKE"
|
||||
smallText = ""
|
||||
}
|
||||
if (type == "strikeOut") {
|
||||
bigText = "STRIKE OUT!"
|
||||
smallText = ""
|
||||
}
|
||||
if (type == "ball") {
|
||||
bigText = "BALL"
|
||||
smallText = ""
|
||||
}
|
||||
if (type == "ballFour") {
|
||||
bigText = "BALL FOUR!"
|
||||
smallText = ""
|
||||
}
|
||||
if (score.outs == 3) {
|
||||
bigText = "GAME OVER"
|
||||
smallText = "Hits: " + score.hits;
|
||||
|
||||
}
|
||||
textSize(32);
|
||||
textAlign(CENTER, CENTER);
|
||||
text(bigText, 250, 350);
|
||||
textStyle(NORMAL);
|
||||
textSize(24);
|
||||
text(smallText, 250, 400);
|
||||
}
|
||||
}
|
||||
|
||||
function resetGame() {
|
||||
bat.swung = false; // 揮棒記錄
|
||||
messageOn = false; // 訊息關閉
|
||||
returnBat();
|
||||
returnBall();
|
||||
if (score.outs == 3) {
|
||||
resetScore();
|
||||
}
|
||||
}
|
||||
|
||||
function pitcherPos(dir) { // 移動投手位置
|
||||
if (ball.status == "ready") {
|
||||
if (dir == "L") {
|
||||
ball.desiredX -= 4;
|
||||
ball.desiredX = constrain(ball.desiredX, 150, 350);
|
||||
}
|
||||
if (dir == "R") {
|
||||
ball.desiredX += 4;
|
||||
ball.desiredX = constrain(ball.desiredX, 150, 350);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function udpateBall() { // 球的移動邏輯
|
||||
// change pitcher position
|
||||
pitcherPos(pitcherDir);
|
||||
// move ball to desired x, y
|
||||
ball.desiredX += constrain(ball.desiredXVel, -500, 1000);
|
||||
if (frameCount % 4) {
|
||||
ball.desiredX += random(-ball.randomX, ball.randomX);
|
||||
}
|
||||
ball.velX = (ball.desiredX - ball.x) * ball.speed;
|
||||
ball.velY = (ball.desiredY - ball.y) * ball.speed;
|
||||
if (abs(ball.desiredX - ball.x) < 1) {
|
||||
ball.x = ball.desiredX;
|
||||
}
|
||||
if (abs(ball.desiredY - ball.y) < 1) {
|
||||
ball.y = ball.desiredY;
|
||||
}
|
||||
if (ball.y > 900 && ball.status == "pitching") { // 球被捕手接到了
|
||||
ball.status = "catched";
|
||||
catchSFX.play();
|
||||
ball.desiredXVel = 0;
|
||||
if (ball.strike || bat.swung) {
|
||||
score.strikes++;
|
||||
messageOn = true;
|
||||
messageType = "strike";
|
||||
console.log("strikes: " + score.strikes);
|
||||
if (score.strikes == 3) { // strike out
|
||||
messageOn = true;
|
||||
messageType = "strikeOut";
|
||||
outSFX.play();
|
||||
score.outs++;
|
||||
if (score.outs == 3) {
|
||||
gameOverSFX.play();
|
||||
}
|
||||
score.strikes = 0;
|
||||
score.balls = 0;
|
||||
}
|
||||
ball.strike = false;
|
||||
} else {
|
||||
score.balls++;
|
||||
messageOn = true;
|
||||
messageType = "ball";
|
||||
console.log("balls: " + score.balls);
|
||||
if (score.balls == 4) { // ball four
|
||||
messageOn = true;
|
||||
messageType = "ballFour";
|
||||
ballFourSFX.play();
|
||||
score.hits++;
|
||||
score.balls = 0;
|
||||
score.strikes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
ball.x += ball.velX;
|
||||
ball.y += ball.velY;
|
||||
ball.angle += ball.spinVel;
|
||||
}
|
||||
|
||||
function ballDrop() { // 判斷球落點!
|
||||
if (ball.status == "flying") {
|
||||
ball.desiredXVel = 0;
|
||||
let ballDrop = calAngle(250, 700, ball.x, ball.y);
|
||||
if (ballDrop < -0.78 && ballDrop > -2.36) {
|
||||
messageOn = true;
|
||||
messageType = "hit";
|
||||
console.log("Fair Ball: " + ballDrop);
|
||||
fairBallSFX.play();
|
||||
score.hits++;
|
||||
score.balls = 0;
|
||||
score.strikes = 0;
|
||||
} else {
|
||||
messageOn = true;
|
||||
messageType = "foul";
|
||||
console.log("Foul Ball: " + ballDrop);
|
||||
if (score.strikes < 2) {
|
||||
score.strikes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function calculatePitch() { // 計算球路
|
||||
if (ball.status == "ready" && pitchInput.c == true && pitchInput.e == true && pitchInput.g == true) { // Check C, E, G all pressed
|
||||
// calculate spin
|
||||
let spin = pitchInput.gVel - pitchInput.cVel // C & G control spin
|
||||
spin = map(spin, -0.5, 0.5, -1, 1, true);
|
||||
ball.desiredXVel = spin * -120;
|
||||
ball.spinVel = spin;
|
||||
// calculate speed
|
||||
let speed = (pitchInput.cVel + pitchInput.eVel * 2 + pitchInput.gVel) / 4;
|
||||
speed = map(speed, 0.2, 0.8, 0.01, 0.06, true);
|
||||
ball.speed = speed;
|
||||
ball.randomX = map(abs(spin), 0, 0.5, 220, 0, true) * map(speed, 0.01, 0.03, 1, 0, true) // the less spin & the less speed, the more random X movement
|
||||
console.log(abs(spin), speed);
|
||||
ball.desiredX += spin * map(speed, 0.01, 0.07, 800, -50, true);
|
||||
|
||||
// if input too slow
|
||||
let duration = max([pitchInput.cTime, pitchInput.eTime, pitchInput.gTime]) - min([pitchInput.cTime, pitchInput.eTime, pitchInput.gTime]);
|
||||
console.log(duration);
|
||||
if (duration > 100) {
|
||||
ball.speed = 0.007; // 輸入期間超過 100ms 的話,球變超慢
|
||||
ball.desiredXVel = ball.desiredXVel * 0.05; // 球變不曲
|
||||
ball.spinVel = ball.spinVel * 0.2 // 球變不曲
|
||||
ball.randomX = 0;
|
||||
}
|
||||
|
||||
// pitch!
|
||||
ball.status = "pitching";
|
||||
console.log("pitch!")
|
||||
pitchBall();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function flyBall(batAng, batVel, ballX, ballY, ballSpeed) { // 球被打到了,計算飛行方向
|
||||
ball.status = "flying";
|
||||
ball.randomX = 0;
|
||||
let flyDist = 1000; // fly distance
|
||||
ball.desiredX = flyDist * cos(batAng - 1.4) + ballX;
|
||||
ball.desiredXVel = 10 * cos(batAng - 1);
|
||||
ball.desiredY = flyDist * sin(batAng - 1.4) + ballY;
|
||||
ball.spinVel += cos(batAng - 1);
|
||||
ball.speed = 0.03; //
|
||||
setTimeout(ballDrop, 600); // 0.6 秒後判斷球落點
|
||||
}
|
||||
|
||||
function pitchBall() {
|
||||
ball.desiredY = 1300; // 投球就是將球想要去的 y 座標設到 1300
|
||||
swishSFX.play();
|
||||
}
|
||||
|
||||
function returnBall() { // return the ball to original position
|
||||
pitchInput.c = false;
|
||||
pitchInput.e = false;
|
||||
pitchInput.g = false;
|
||||
ball.speed = 0.2;
|
||||
ball.spinVel = random(-0.02, 0.02);
|
||||
ball.desiredX = ball.startX;
|
||||
ball.desiredXVel = 0;
|
||||
ball.desiredY = ball.startY;
|
||||
pitcherDir = "C";
|
||||
ball.hit = false;
|
||||
ball.status = "ready";
|
||||
ball.strike = false;
|
||||
ball.randomX = 0;
|
||||
}
|
||||
|
||||
function drawBall() {
|
||||
push();
|
||||
translate(ball.x, ball.y);
|
||||
rotate(ball.angle);
|
||||
imageMode(CENTER);
|
||||
image(ballImg, 0, 0, ball.size, ball.size);
|
||||
pop();
|
||||
}
|
||||
|
||||
function updateBat() {
|
||||
bat.vel = (bat.desiredAngle - bat.angle) * bat.speed;
|
||||
bat.angle = bat.angle + bat.vel;
|
||||
if (abs(bat.angle - bat.desiredAngle) < 0.01) {
|
||||
bat.angle = bat.desiredAngle;
|
||||
}
|
||||
}
|
||||
|
||||
function drawBat() {
|
||||
push();
|
||||
translate(170, 600);
|
||||
rotate(bat.angle);
|
||||
image(batImg, 40, -40, 105, 105);
|
||||
pop();
|
||||
}
|
||||
|
||||
function swingBat(vel) {
|
||||
if (bat.status == "ready") {
|
||||
bat.speed = map(vel, 0.2, 0.8, 0.1, 0.4, true);
|
||||
console.log(bat.speed);
|
||||
bat.desiredAngle = bat.endAngle;
|
||||
bat.status = "swinging";
|
||||
swingSFX.play();
|
||||
bat.swung = true;
|
||||
setTimeout(returnBat, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function returnBat() {
|
||||
bat.desiredAngle = bat.startAngle;
|
||||
bat.speed = 0.1;
|
||||
bat.status = "ready";
|
||||
}
|
||||
|
||||
function drawField() {
|
||||
stroke(30);
|
||||
line(250, 700, 0, 450);
|
||||
line(250, 700, 500, 450);
|
||||
imageMode(CENTER);
|
||||
image(plateImg, 250, 650, 110, 110);
|
||||
|
||||
}
|
||||
|
||||
function resetScore() {
|
||||
score.strikes = 0;
|
||||
score.balls = 0;
|
||||
score.outs = 0;
|
||||
score.hits = 0;
|
||||
}
|
||||
|
||||
function noteOn(note, vel, ms) {
|
||||
if (note == 36) { // C3
|
||||
resetScore();
|
||||
resetGame();
|
||||
}
|
||||
if (note == 48) { // C3
|
||||
swingBat(vel);
|
||||
}
|
||||
if (note == 60) {
|
||||
resetGame();
|
||||
}
|
||||
if (note == 72) {
|
||||
pitchInput.c = true;
|
||||
pitchInput.cVel = vel;
|
||||
pitchInput.cTime = ms;
|
||||
}
|
||||
if (note == 76) {
|
||||
pitchInput.e = true;
|
||||
pitchInput.eVel = vel;
|
||||
pitchInput.eTime = ms;
|
||||
}
|
||||
if (note == 79) {
|
||||
pitchInput.g = true;
|
||||
pitchInput.gVel = vel;
|
||||
pitchInput.gTime = ms;
|
||||
}
|
||||
if (note == 73) {
|
||||
pitcherDir = "L";
|
||||
}
|
||||
if (note == 75) {
|
||||
pitcherDir = "R";
|
||||
}
|
||||
}
|
||||
|
||||
function noteOff(note, vel, ms) {
|
||||
if (note == 73) {
|
||||
pitcherDir = "C";
|
||||
}
|
||||
if (note == 75) {
|
||||
pitcherDir = "C";
|
||||
}
|
||||
}
|
||||
|
||||
function inputChanged() {
|
||||
midiIn.removeListener();
|
||||
midiIn = WebMidi.inputs[midiSelectSlider.value()];
|
||||
midiIn.addListener('noteon', "all", function (e) {
|
||||
console.log(e.note.number, e.velocity, e.timestamp);
|
||||
noteOn(e.note.number, e.velocity, e.timestamp); // number:
|
||||
});
|
||||
midiIn.addListener('noteoff', "all", function (e) {
|
||||
noteOff(e.note.number, e.velocity, e.timestamp);
|
||||
})
|
||||
console.log(midiIn.name);
|
||||
select("#device").html(midiIn.name);
|
||||
};
|
||||
|
||||
function mouseClicked() {
|
||||
resetGame();
|
||||
userStartAudio();
|
||||
}
|
||||
|
||||
|
||||
// line-circle collision
|
||||
|
||||
function lineCircle(x1, y1, x2, y2, cx, cy, r) {
|
||||
|
||||
// is either end INSIDE the circle?
|
||||
// if so, return true immediately
|
||||
let inside1 = pointCircle(x1, y1, cx, cy, r);
|
||||
let inside2 = pointCircle(x2, y2, cx, cy, r);
|
||||
if (inside1 || inside2) return true;
|
||||
|
||||
// get length of the line
|
||||
let distX = x1 - x2;
|
||||
let distY = y1 - y2;
|
||||
let len = sqrt((distX * distX) + (distY * distY));
|
||||
|
||||
// get dot product of the line and circle
|
||||
let dot = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / pow(len, 2);
|
||||
|
||||
// find the closest point on the line
|
||||
let closestX = x1 + (dot * (x2 - x1));
|
||||
let closestY = y1 + (dot * (y2 - y1));
|
||||
|
||||
// is this point actually on the line segment?
|
||||
// if so keep going, but if not, return false
|
||||
let onSegment = linePoint(x1, y1, x2, y2, closestX, closestY);
|
||||
if (!onSegment) return false;
|
||||
|
||||
// optionally, draw a circle at the closest
|
||||
// point on the line
|
||||
|
||||
|
||||
// get distance to closest point
|
||||
distX = closestX - cx;
|
||||
distY = closestY - cy;
|
||||
let distance = sqrt((distX * distX) + (distY * distY));
|
||||
|
||||
if (distance <= r) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// POINT/CIRCLE
|
||||
function pointCircle(px, py, cx, cy, r) {
|
||||
|
||||
// get distance between the point and circle's center
|
||||
// using the Pythagorean Theorem
|
||||
let distX = px - cx;
|
||||
let distY = py - cy;
|
||||
let distance = sqrt((distX * distX) + (distY * distY));
|
||||
|
||||
// if the distance is less than the circle's
|
||||
// radius the point is inside!
|
||||
if (distance <= r) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// LINE/POINT
|
||||
function linePoint(x1, y1, x2, y2, px, py) {
|
||||
|
||||
// get distance from the point to the two ends of the line
|
||||
let d1 = dist(px, py, x1, y1);
|
||||
let d2 = dist(px, py, x2, y2);
|
||||
|
||||
// get the length of the line
|
||||
let lineLen = dist(x1, y1, x2, y2);
|
||||
|
||||
// since floats are so minutely accurate, add
|
||||
// a little buffer zone that will give collision
|
||||
let buffer = 0.1; // higher # = less accurate
|
||||
|
||||
// if the two distances are equal to the line's
|
||||
// length, the point is on the line!
|
||||
// note we use the buffer here to give a range,
|
||||
// rather than one #
|
||||
if (d1 + d2 >= lineLen - buffer && d1 + d2 <= lineLen + buffer) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function calAngle(cx, cy, ex, ey) {
|
||||
var dy = ey - cy;
|
||||
var dx = ex - cx;
|
||||
var theta = Math.atan2(dy, dx); // range (-PI, PI]
|
||||
return theta;
|
||||
}
|
Reference in New Issue
Block a user