搜索了一下笛卡尔和瑞典公主的浪漫爱情故事,其中提到了一个心形函数,但是看起来像极坐标系的函数。以下借助大佬的数学公式和其他大佬的代码实现,移植java Swing和android平台进行实现。马上就要2.14号了,祝有情人终成眷属,举案齐眉,相敬如宾,白头偕老。
https://mathworld.wolfram.com/HeartCurve.html
x = 16 sin 3 t x=16\,\sin^{3}t x=16sin3t
y = 13 cos t − 5 cos ( 2 t ) − 2 cos ( 3 t ) − cos ( 4 t ) y=13\,\cos t - 5\cos(2t)-2\cos(3t)-\cos(4t) y=13cost−5cos(2t)−2cos(3t)−cos(4t)
效果图如下:
可惜swing的graphics画图方法参数只支持int类型,导致曲线不太平滑,可以在android里面绘制更平滑的曲线,android的画线方法支持浮点数。此曲线的绘制参考了不少大佬的博客代码,在android和swing里面均相应转化成功。后面的坐标轴可以去掉,加上只是为了参考。
如果只是个静态图也有些乏味,尝试让曲线绘制过程变的可见,加个延时。
import java.awt.*
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.JPanel
// 动画版的画心,从上边顺时针绘制
class HeartFrameKt2 : JFrame() {
init {
val panel = HeartPanel()
val jButton = JButton("refresh")
add(panel, BorderLayout.CENTER)
add(jButton, BorderLayout.SOUTH)
var thread = Thread {
while (true) {
var refresh = true
EventQueue.invokeLater {
refresh = panel.refresh()
}
Thread.sleep(25)
}
}
thread.start()
jButton.addActionListener {
panel.clear()
}
}
inner class HeartPanel : JPanel() {
var i = 0.0
var list = mutableListOf<OkPoint>()
override fun paintComponent(g: Graphics?) {
super.paintComponent(g)
var g2: Graphics2D = g as Graphics2D
g2.translate(width / 2, height / 2)
drawAxis(g)
//修线条粗细
g2.stroke = BasicStroke(3.0f)
g2.color = Color.RED
val inc = Math.PI / 90
if (i <= 2 * Math.PI) {
var x = getX(20, i.toFloat())
var y = getY(20, i.toFloat())
var p = OkPoint(x, y)
list.add(p)
i += inc
}
for (i in list.indices) {
if (i < list.size - 1) {
var p0 = list.get(i)
var p1 = list.get(i + 1)
g2.drawLine(p0.x.toInt(), p0.y.toInt(), p1.x.toInt(), p1.y.toInt())
}
}
g2.translate(-width / 2, -height / 2)
}
fun getX(zoom: Int, theta: Float): Double {
return zoom * (16 * Math.pow(Math.sin(theta.toDouble()), 3.0))
}
fun getY(zoom: Int, theta: Float): Double {
return (-zoom
* (13 * Math.cos(theta.toDouble()) - 5 * Math.cos((2 * theta).toDouble()) - (2
* Math.cos((3 * theta).toDouble())) - Math.cos((4 * theta).toDouble())))
}
fun drawAxis(g: Graphics?) {
var g2: Graphics2D = g as Graphics2D
g2.stroke = BasicStroke(1.0f)
g2.color = Color.BLACK
g2.drawLine(-width / 2, 0, width / 2, 0)
g2.drawLine(0, -height / 2, 0, height / 2)
//unit=10,vertical line,x1,y1,x2,y2
// short line, long line
val sl = 5
val ll = 10
//x axis
for (i in 0..width / 2 step 10) {
if (i % 50 == 0) {
g2.drawLine(i, 0, i, -ll)
g2.drawLine(-i, 0, -i, -ll)
} else {
g2.drawLine(i, 0, i, -sl)
g2.drawLine(-i, 0, -i, -sl)
}
}
//y axis
for (i in 0..height / 2 step 10) {
if (i % 50 == 0) {
g2.drawLine(0, i, ll, i)
g2.drawLine(0, -i, ll, -i)
} else {
g2.drawLine(0, i, sl, i)
g2.drawLine(0, -i, sl, -i)
}
}
}
fun refresh():Boolean {
return if (i <= 2 * Math.PI) {
repaint()
true
} else {
false
}
}
fun clear() {
i = 0.0
list.clear()
}
}
inner class OkPoint(var x: Double, var y: Double)
}
fun main(args: Array<String>) {
var frame = HeartFrameKt2()
frame.apply {
setSize(1500, 800)
title = "Kotlin heart"
setLocationRelativeTo(null) // Center the frame
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
isVisible = true
}
}
Android版本
public class ValentineView extends View {
Paint paint;
int w, h;
Bitmap flower;
Canvas hearCanvas;
Bitmap bmpHeart;
float leftAngle = (float)( 2*Math.PI);
float rightAngle = 0;
double inc = Math.PI / 45;
int zoom = 1;
float finalTxSize;
public ValentineView(Context context, AttributeSet attrs) {
super(context, attrs);
float density = getResources().getDisplayMetrics().density;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.parseColor("#FFDB9C"));
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setTextSize(/*density*22*/0);
paint.setStrokeWidth(2);
paint.setDither(true);
flower = BitmapFactory.decodeResource(getResources(), R.mipmap.heart);
finalTxSize = density * 22;
}
public ValentineView(Context context) {
this(context, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wM = MeasureSpec.getMode(widthMeasureSpec);
int hM = MeasureSpec.getMode(heightMeasureSpec);
if (wM == MeasureSpec.EXACTLY) {
w = MeasureSpec.getSize(widthMeasureSpec);
}
if (hM == MeasureSpec.EXACTLY) {
h = MeasureSpec.getSize(heightMeasureSpec);
}
setMeasuredDimension(w, h);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
hearCanvas = new Canvas();
bmpHeart = Bitmap.createBitmap(w, h, Config.ARGB_8888);
hearCanvas.setBitmap(bmpHeart);
// 计算需要放大的倍数,铺满屏幕宽度
zoom = (int) Math.ceil(w / 2.0 / 16.0 - 4);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawHeart(canvas);
canvas.drawBitmap(bmpHeart, 0, 0, paint);
if (rightAngle > Math.PI || leftAngle < Math.PI){
Log.d("xxx", "draw hear ok");
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("情人节快乐", w/2, h/2, paint);
paint.setTextAlign(Paint.Align.LEFT);
if (paint.getTextSize() <= finalTxSize) {
paint.setTextSize(paint.getTextSize()+1);
postInvalidateDelayed(25);
}else{
Log.d("xxx", "draw txt ok");
}
}
}
private void drawHeart(Canvas canvas) {
hearCanvas.save();
hearCanvas.translate(w / 2, h / 2);//
float leftX = (float) getX(zoom, leftAngle);
float leftY = (float) getY(zoom, leftAngle);
float rightX = (float) getX(zoom, rightAngle);
float rightY = (float) getY(zoom, rightAngle);
//Log.d("xxx", String.format("x:%s, y:%s", x, y));
hearCanvas.save();
hearCanvas.translate(-flower.getWidth()/2, -flower.getHeight()/2);
hearCanvas.drawBitmap(flower, leftX, leftY, paint);
hearCanvas.drawBitmap(flower, rightX, rightY, paint);
hearCanvas.restore();
hearCanvas.restore();
// right half
if(rightAngle <= Math.PI){
rightAngle += inc;
//postInvalidateDelayed(50);
}
// left half
if(leftAngle >= Math.PI){
leftAngle -= inc;
postInvalidateDelayed(50);
}
}
double getX(int zoom, float theta) {
return zoom * (16 * Math.pow(Math.sin(theta), 3));
}
double getY(int zoom, float theta) {
return -zoom
* (13 * Math.cos(theta) - 5 * Math.cos(2 * theta) - 2
* Math.cos(3 * theta) - Math.cos(4 * theta));
}
}
更多代码,请参考github:https://github.com/ximen502/SwingLearn,Swing中用kotlin还挺不错,kotlin确实简洁而强大,和java形成很好的互补。
Swing中的JPanel有2个作用一个是作为子面板容器,另一个是可以用于自定义组件(view),重写方法实现自定义绘制。
参考了用户qq_32250025的心形曲线公式博客,感谢。