• 作者:老汪软件技巧
  • 发表时间:2024-08-29 04:02
  • 浏览量:

源码

/buglas/canv…

学习目标知识点1-多角星简介

多角星就是有多个角的星形,其中有两种角:

内凹角有两种定义方式:

咱们依次说一下这两种绘制方法。

1-1-绘制自定义内圆半径的多角星

已知:

求:点A、B

解:

若求点A、B,只需要求出A、B的弧度即可。

设点A的弧度为α,则:

α=2π/n

所以:

A=(cosα*r1,sinα*r1)

设点B的弧度为θ,则由多角星的定义规则可知:

θ=α*1.5

所以:

B=(cosθ*r2,sinθ*r2)

求出点A、点B后,将其分别旋转(360/n)°,便可算出其余的顶点。

1-2-绘制共线多角星

绘制这种多角星的核心就是将两组外凸角的顶点连线,如直线AB、直线CD,然后以两条直线的交点为内凹角的顶点。

在计算两条直线的交点的时候,需要用到直线的一般式:

Ax+By+C=0

已知两点,可以求出直线的一般式。

比如,已知:两点M(x1,y1),N(x2,y2)

则直线MN的一般式为:

(y2-y1)*x-(x2-x1)*y+(x2-x1)*y1-(y2-y1)*x1=0

从上式中可以提取到A、B、C:

A=△y=y2-y1
B=-△x=-(x2-x1)
C=△x*y1-△y*x1

以此原理,我们可以求出直线AB、CD的一般式:

AB:A1x+B1y+C1=0 
CD:A2x+B2y+C2=0

两条直线相交的条件是:

A1B2-A2B1≠0

在此条件下,其交点为:

p.x=(B1C2-C1B2)/(A1B2-A2B1)
p.y=(A2C1-A1C2)/(A1B2-A2B1)

对于交点公式的推导,大家可以参考此文章:《漫谈直线之斜截式和一般式》。

前不久我在初二的数学课本上也看到了相关知识,挺有趣的,分享给大家

北师大版的《初中数学八年级上册》第112页的“读一读”:

我们套用读一读里的这个公式就可以算出交点:

a11、a21对应的是直线一般式里的A;a12、a22对应的是B;b1、b2 对应 -C。

现在思路已通,接下来咱们封装一个直线对象。

2-Line2

Line2 对象是一个数学方法对象,不用于绘图。

import { Vector2 } from './Vector2'
type IntersectCallback=(data:Vector2)=>Vector2|null
class Line2 {
  // 起点
    start = new Vector2()
  // 终点
    end = new Vector2()
  // 类型
    readonly Line2 = true
    constructor(start?:Vector2, end?:Vector2) {
        start&&(this.start = start)
        end&&(this.end = end)
    }
    set(start: Vector2, end: Vector2) {
        this.start.copy(start)
        this.end.copy(end)
        return this
    }
    // 直线一般式:Ax+By+C=0,返回A、B、C
    ABC() {
        const { start, end } = this
        const [dx, dy] = [end.x - start.x, end.y - start.y]
        const [A, B] = [dy, -dx]
        return [A, B, -(A * start.x + B * start.y)]
    }
    // 与另一条直线的交点
    intersect(line: Line2,callback:IntersectCallback=(p:Vector2)=>p):Vector2|null  {
        const [A1, B1, C1, A2, B2, C2] = [...this.ABC(), ...line.ABC()]
        const sub = A1 * B2 - A2 * B1
        if (sub) {
      const p = new Vector2(
                (B1 * C2 - C1 * B2) / sub,
                (A2 * C1 - A1 * C2) / sub
            )
            // 相交,默认返回交点p,可以自定义过滤函数,过滤交点
      return callback(p)
        } else {
            // 平行
            return null
        }
    }
}
export { Line2 }

Line2 对象是通过start 和end 两点确定的,因为两点确定一条直线。

intersect(line,callback) 是利用直线一般式求交的方法,其第二个参数是一个回调函数,可以对计算出的交点做过滤,默认是不过滤的。

接下来,再封装一个线段对象,因为我们要画的多角星是由线段组成的。

3-Segment2

Segment2对象也是一个数学方法对象,不用于绘图,它继承自Line2对象,因为线段也属于直线。

import { Line2 } from './Line2'
import { Vector2 } from './Vector2'
const _v = new Vector2()
/* 此线段是有方向概念的,所以就有了法线、正反面等概念。*/
class Segment2 extends Line2{
  /* 线段长度 */
  length() {
        return this.vector().length()
    }
    /* 线段长度的平方 */
    lengthSq() {
        return this.vector().lengthSq()
    }
  /* 方向:单位向量 */
    direction() {
        return this.vector().normalize()
    }
  /* 向量:end-start */
    vector() {
        const { start, end } = this
        return new Vector2(end.x - start.x, end.y - start.y)
    }
  /* 线段与线段交点 */
    intersect(seg: Segment2):Vector2|null {
    return super.intersect(seg,(p:Vector2)=>{
      if (this.isPointIn(p) && seg.isPointIn(p)) {
        return p
      }
      return null
    })
    }
  /* 法线 */
    normal() {
        const { start, end } = this
        const { x, y } = end.clone().sub(start)
        return new Vector2(y, -x).normalize()
    }
  /* 线段所在的直线上一点是否在线段上 */
    isPointIn(p: Vector2) {
        const { start, end } = this
        const len = _v.subVectors(end, start).lengthSq()
        const ls = _v.subVectors(p, start).lengthSq()
        const le = _v.subVectors(p, end).lengthSq()
        if (ls <= len && le <= len) {
            return true
        }
        return false
    }
}
export { Segment2 }

此处的Segment2 对象和数学概念上的线段还有点不同,它有正反面,具备从起点到终点的方向性,拥有法线的概念。

这样可以更方便的做一些图形学方面的测试,把一些复杂的三维算法简化成二维,比如我们后面要说的碰撞反弹。

4-NStarGeometry

NStarGeometry是多角星的几何对象,它继承自PolyGeometry 对象。

import { Line2 } from "../math/Line2";
import { Vector2 } from "../math/Vector2";
import { PolyGeometry } from "./PolyGeometry";
type R2Type = number | 'auto'
const PI = Math.PI
const PI2 = PI * 2
class NStarGeometry extends PolyGeometry{
  // 角数
  count:number
  // 外圆半径
    r1:number
  // 内圆半径
    r2: number
  // 是否逆时针绘图
  counterclockwise:boolean
  // 闭合路径
    closePath = true
  constructor(r1:number=300,r2:R2Type='auto',count:number=5,counterclockwise:boolean=false){
    super()
    this.r1=r1
    this.count=count
    this.counterclockwise=counterclockwise
    this.r2=r2 === 'auto'?this.autoR2():r2
    this.updatePosition()
  }
  /* 计算相邻的两个内角顶点共线的内圆半径 */
    autoR2() {
        const { r1, count } = this
        if (count < 5) {
            return r1 / 3
        }
        const space = PI2 / count
        const [A, B, C, D] = [0, 2, 1, 3].map((n) => {
            const ang = space * n
            return new Vector2(Math.cos(ang) * r1, Math.sin(ang) * r1)
        })
        const intersection = new Line2(A, B).intersect(new Line2(C, D))
        if (intersection) {
            return intersection.length()
        }
        return 0
    }
    // 更新点位
    updatePosition() {
        const { r1, r2, count,counterclockwise } = this
    const dir=counterclockwise?-1:1
        const space = PI2 / count*dir
        const halfSpace = PI / count*dir
        const position: number[] = []
        for (let i = 0; i < count; i++) {
            const ang1 = space * i
            const ang2 = ang1 + halfSpace
            position.push(
                Math.cos(ang1) * r1,
                Math.sin(ang1) * r1,
                Math.cos(ang2) * r2,
                Math.sin(ang2) * r2
            )
        }
        this.position = position
    }
  // 克隆
    clone() {
        return new NStarGeometry().copy(this)
    }
  // 拷贝
  copy(nStarGeometry:NStarGeometry){
    const {r1,r2,count,counterclockwise}=nStarGeometry
    super.copy(nStarGeometry)
    this.r1=r1
    this.r2=r2
    this.count=count
    this.counterclockwise=counterclockwise
    return this
  }
}
export {NStarGeometry}

当r2值为'auto'时,会通过autoR2()方法自动计算内角半径。当角数小于5时,r2等于r1的1/3;否则,通过外角连线的交点计算r2,其中的算法咱们之前也说过,不再赘述。

5-绘制多角星

建立一个NStar.vue文件用来测试效果。

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Scene } from '../lmm/core/Scene'
import { NStarGeometry } from '../lmm/geometry/NStarGeometry'
import { StandStyle } from '../lmm/style/StandStyle'
import { Graph2D } from '../lmm/objects/Graph2D'
// 获取父级属性
defineProps({
    size: { type: Object, default: { width: 0, height: 0 } },
})
// 对应canvas 画布的Ref对象
const canvasRef = ref<HTMLCanvasElement>()
/* 场景 */
const scene = new Scene()
/* 多角星 */
const nStarGeometry=new NStarGeometry(200,150,5,true);
const nStarStyle=new StandStyle({
  strokeStyle: '#73e800',
  lineWidth: 10,
  fillStyle: '#fff693',
})
const nStarObj=new Graph2D(nStarGeometry,nStarStyle)
nStarObj.rotate=-Math.PI / 2
scene.add(nStarObj)
onMounted(() => {
    const canvas = canvasRef.value
    if (canvas) {
        scene.setOption({ canvas })
        scene.render()
    }
})
script>
<template>
    <canvas ref="canvasRef" :width="size.width" :height="size.height">canvas>
template>
<style scoped>style>

效果如下:

这是一颗杨桃的形状。

当我们将r2缓存"auto",r2便会根据两组外凸角的顶点连成的两条直线的交点自动计算。

const nStarGeometry=new NStarGeometry(200,'auto',5,true);

效果如下:

总结

这节课的知识点主要就是三角函数、直线的交点,以及图形的封装。

下一篇我们会说第二道题:绘制小球与多边形的碰撞反弹路径。