消消方块阵换皮表情
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

461 lines
13 KiB

import {
_decorator,
Component,
Graphics,
Vec2,
Vec3,
Color,
} from 'cc';
const { ccclass, property } = _decorator;
/**
* 箭头流向线:支持多条路径。
* 主接口 setPaths:传入多条折线(本地坐标),每条线自动使用不同颜色。
*/
@ccclass('ArrowFlowLine')
export class ArrowFlowLine extends Component {
@property({ type: [Vec2], tooltip: '单条路径(本地);代码调用 setPaths 后以此为空则忽略' })
pathPoints: Vec2[] = [];
@property({ tooltip: '仅一条路径且未用 setPaths 时,作为该线箭头颜色', type: Color })
arrowColor: Color = new Color(0, 180, 255, 255);
@property({ tooltip: '箭头长度(像素)', min: 2, max: 80 })
arrowLength: number = 16;
@property({ tooltip: '箭头宽度(像素)', min: 1, max: 40 })
arrowWidth: number = 8;
@property({ tooltip: '箭头头尾比例(0~1)', min: 0, max: 1, step: 0.05 })
arrowSharpness: number = 0.6;
@property({ tooltip: '箭头间距比例(相对箭头长度)', min: 0, max: 5, step: 0.1 })
gapRatio: number = 0.5;
@property({ tooltip: '拐点平滑采样数', min: 2, max: 30 })
smoothSamples: number = 10;
@property({ tooltip: 'Catmull-Rom 张力', min: 0, max: 1, step: 0.05 })
tension: number = 0.85;
@property({ tooltip: '启用流动动画' })
enableFlow: boolean = true;
@property({ tooltip: '流动速度(像素/秒)', min: 1, max: 500 })
flowSpeed: number = 60;
@property({ tooltip: 'true=正向' })
flowForward: boolean = true;
@property({ tooltip: '流动时箭头重绘上限(帧/秒),0 表示不限制;移动端建议 20~30' })
flowMaxRedrawFps: number = 30;
private _graphics: Graphics | null = null;
private _flowOffset: number = 0;
private _dirty: boolean = true;
/** 路径几何变化后需至少重绘一次(静态线只画一次) */
private _geometryChanged: boolean = true;
/** 缓存路径总长,避免每帧遍历 _built */
private _cachedMaxPathLen: number = 0;
/** 流动重绘节流:累计时间 */
private _flowRedrawAccum: number = 0;
/** sampleAtDistance / _drawArrow 复用,避免每箭头 new Vec2 */
private _samplePos = new Vec2();
private _sampleDir = new Vec2();
private _lastArrowFillColor: Color | null = null;
/** 代码设置的路线;非 null 时优先于 pathPoints */
private _pathList: Vec2[][] | null = null;
/** 与 _pathList 等长,每条线颜色 */
private _perRouteColors: Color[] = [];
private _built: BuiltRoute[] = [];
onLoad() {
this._ensureGraphics();
}
onEnable() {
this._dirty = true;
this._geometryChanged = true;
}
update(dt: number) {
if (this._dirty) {
this._rebuildPath();
this._dirty = false;
this._geometryChanged = true;
this._recomputeCachedMaxLen();
}
const g = this._graphics;
if (!g) return;
if (this._built.length === 0) {
g.clear();
this._lastArrowFillColor = null;
return;
}
const period = this.arrowLength * (1 + this.gapRatio);
if (this.enableFlow && this._cachedMaxPathLen > 0 && period >= 1) {
const dir = this.flowForward ? 1 : -1;
this._flowOffset += dir * this.flowSpeed * dt;
if (this._flowOffset > period) this._flowOffset -= period;
if (this._flowOffset < -period) this._flowOffset += period;
}
let shouldRedraw = false;
if (this._geometryChanged) {
shouldRedraw = true;
} else if (!this.enableFlow) {
shouldRedraw = false;
} else {
const maxFps = this.flowMaxRedrawFps;
if (maxFps <= 0) {
shouldRedraw = true;
} else {
this._flowRedrawAccum += dt;
const interval = 1 / maxFps;
if (this._flowRedrawAccum >= interval) {
this._flowRedrawAccum -= interval;
shouldRedraw = true;
}
}
}
if (shouldRedraw) {
this._drawArrows();
this._geometryChanged = false;
}
}
private _recomputeCachedMaxLen() {
let maxLen = 0;
for (const b of this._built) {
if (b.totalLength > maxLen) maxLen = b.totalLength;
}
this._cachedMaxPathLen = maxLen;
}
/**
* 设置多条路径(本地空间 Vec2/Vec3)。每条折线至少 2 个点;每条线颜色自动区分。
*/
setPaths(paths: (Vec2 | Vec3)[][]) {
const list: Vec2[][] = [];
for (const way of paths) {
if (!way?.length) continue;
const pts = way.map(p => new Vec2(p.x, p.y));
if (pts.length >= 2) list.push(pts);
}
if (list.length === 0) {
this._pathList = null;
this._perRouteColors = [];
} else {
this._pathList = list;
// 使用固定颜色数组,超出长度则循环使用
this._perRouteColors = list.map((_, i) => fixedRouteColor(i));
}
this._dirty = true;
}
/** 单条路径;整线使用 arrowColor */
setPath(points: (Vec2 | Vec3)[]) {
const pts = points.map(p => new Vec2(p.x, p.y));
if (pts.length < 2) {
this._pathList = null;
this._perRouteColors = [];
} else {
this._pathList = [pts];
this._perRouteColors = [this.arrowColor.clone()];
}
this._dirty = true;
}
/**
* 旧版:所有起点接所有终点连成一条线(本地坐标),颜色为 arrowColor
*/
setRoute(starts: (Vec2 | Vec3)[], ends: (Vec2 | Vec3)[]) {
const pts: Vec2[] = [];
for (const p of starts) pts.push(new Vec2(p.x, p.y));
for (const p of ends) pts.push(new Vec2(p.x, p.y));
this.setPath(pts);
}
private _ensureGraphics() {
this._graphics = this.getComponent(Graphics);
if (!this._graphics) {
this._graphics = this.addComponent(Graphics);
}
this._graphics.lineWidth = 0;
}
private _collectPathInputs(): { points: Vec2[]; color: Color }[] {
if (this._pathList !== null && this._pathList.length > 0) {
const out: { points: Vec2[]; color: Color }[] = [];
for (let i = 0; i < this._pathList.length; i++) {
const color = this._perRouteColors[i]?.clone() ?? this.arrowColor.clone();
out.push({
points: this._pathList[i].map(p => p.clone()),
color,
});
}
return out;
}
if (this.pathPoints.length >= 2) {
return [{
points: this.pathPoints.map(p => p.clone()),
color: this.arrowColor.clone(),
}];
}
return [];
}
private _rebuildPath() {
this._built = [];
const inputs = this._collectPathInputs();
for (const { points: pts, color } of inputs) {
const smoothPts = this._catmullRomSmooth(pts);
if (smoothPts.length < 2) continue;
const segments: PathSegment[] = [];
let accLen = 0;
for (let i = 0; i < smoothPts.length - 1; i++) {
const a = smoothPts[i];
const b = smoothPts[i + 1];
const dx = b.x - a.x;
const dy = b.y - a.y;
const len = Math.sqrt(dx * dx + dy * dy);
if (len < 0.01) continue;
segments.push({
start: a,
end: b,
length: len,
startAccLen: accLen,
dirX: dx / len,
dirY: dy / len,
});
accLen += len;
}
if (segments.length === 0) continue;
this._built.push({ segments, totalLength: accLen, color });
}
}
private _catmullRomSmooth(points: Vec2[]): Vec2[] {
const n = points.length;
if (n < 2) return points.slice();
if (n === 2) return points.slice();
const result: Vec2[] = [];
const t = this.tension;
const samples = this.smoothSamples;
const getPoint = (i: number): Vec2 => {
if (i < 0) {
const dx = points[0].x - points[1].x;
const dy = points[0].y - points[1].y;
return new Vec2(points[0].x + dx, points[0].y + dy);
}
if (i >= n) {
const dx = points[n - 1].x - points[n - 2].x;
const dy = points[n - 1].y - points[n - 2].y;
return new Vec2(points[n - 1].x + dx, points[n - 1].y + dy);
}
return points[i];
};
for (let seg = 0; seg < n - 1; seg++) {
const p0 = getPoint(seg - 1);
const p1 = getPoint(seg);
const p2 = getPoint(seg + 1);
const p3 = getPoint(seg + 2);
for (let s = 0; s < samples; s++) {
const tt = s / samples;
const pos = catmullRomPoint(p0, p1, p2, p3, tt, t);
if (result.length > 0) {
const last = result[result.length - 1];
const dx = pos.x - last.x;
const dy = pos.y - last.y;
if (dx * dx + dy * dy < 0.01) continue;
}
result.push(pos);
}
}
result.push(points[n - 1].clone());
return result;
}
private _drawArrows() {
const g = this._graphics;
if (!g) return;
g.clear();
this._lastArrowFillColor = null;
const period = this.arrowLength * (1 + this.gapRatio);
if (period < 1) return;
let dist0 = this._flowOffset;
dist0 = ((dist0 % period) + period) % period;
for (const br of this._built) {
if (br.totalLength < 1 || br.segments.length === 0) continue;
let dist = dist0;
while (dist < br.totalLength) {
if (this._sampleAtDistanceInto(br.segments, br.totalLength, dist, this._samplePos, this._sampleDir)) {
this._drawArrow(g, this._samplePos, this._sampleDir, br.color);
}
dist += period;
}
}
}
private _sampleAtDistanceInto(
segments: PathSegment[],
totalLength: number,
d: number,
outPos: Vec2,
outDir: Vec2,
): boolean {
if (d < 0 || d > totalLength) return false;
for (const seg of segments) {
const localD = d - seg.startAccLen;
if (localD >= 0 && localD <= seg.length) {
const t = seg.length > 0 ? localD / seg.length : 0;
outPos.x = seg.start.x + (seg.end.x - seg.start.x) * t;
outPos.y = seg.start.y + (seg.end.y - seg.start.y) * t;
outDir.x = seg.dirX;
outDir.y = seg.dirY;
return true;
}
}
return false;
}
private _drawArrow(g: Graphics, pos: Vec2, dir: Vec2, color: Color) {
// 简化为截图中那种小实心三角箭头
const len = this.arrowLength;
const halfW = this.arrowWidth / 2;
const dx = dir.x;
const dy = dir.y;
const nx = -dy;
const ny = dx;
const cx = pos.x;
const cy = pos.y;
// 箭头尖端:沿着 dir 方向
const tipX = cx + dx * len * 0.5;
const tipY = cy + dy * len * 0.5;
// 箭头底边中心:反向一点
const baseX = cx - dx * len * 0.3;
const baseY = cy - dy * len * 0.3;
const leftX = baseX + nx * halfW;
const leftY = baseY + ny * halfW;
const rightX = baseX - nx * halfW;
const rightY = baseY - ny * halfW;
if (!this._lastArrowFillColor || !Color.equals(this._lastArrowFillColor, color)) {
g.fillColor = color;
g.strokeColor = color;
if (!this._lastArrowFillColor) {
this._lastArrowFillColor = new Color();
}
this._lastArrowFillColor.set(color);
}
g.moveTo(tipX, tipY);
g.lineTo(leftX, leftY);
g.lineTo(rightX, rightY);
g.close();
g.fill();
}
}
interface PathSegment {
start: Vec2;
end: Vec2;
length: number;
startAccLen: number;
dirX: number;
dirY: number;
}
interface BuiltRoute {
segments: PathSegment[];
totalLength: number;
color: Color;
}
const kRouteColors: Color[] = [
Color.WHITE,
Color.BLACK,
Color.RED,
Color.GREEN,
Color.CYAN,
Color.MAGENTA,
Color.YELLOW,
];
function fixedRouteColor(index: number): Color {
if (kRouteColors.length === 0) {
return Color.WHITE;
}
const c = kRouteColors[index % kRouteColors.length];
return c.clone();
}
function hsvToColor(h: number, s: number, v: number): Color {
const hh = (h % 1 + 1) % 1 * 6;
const i = Math.floor(hh);
const f = hh - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
let r = 0;
let g = 0;
let b = 0;
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
default: r = v; g = p; b = q; break;
}
return new Color(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), 255);
}
function catmullRomPoint(
p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2,
t: number, tension: number,
): Vec2 {
const s = (1 - tension) / 2;
const t2 = t * t;
const t3 = t2 * t;
const ax = -s * p0.x + (2 - s) * p1.x + (s - 2) * p2.x + s * p3.x;
const bx = 2 * s * p0.x + (s - 3) * p1.x + (3 - 2 * s) * p2.x - s * p3.x;
const cx = -s * p0.x + s * p2.x;
const dx = p1.x;
const ay = -s * p0.y + (2 - s) * p1.y + (s - 2) * p2.y + s * p3.y;
const by = 2 * s * p0.y + (s - 3) * p1.y + (3 - 2 * s) * p2.y - s * p3.y;
const cy = -s * p0.y + s * p2.y;
const dy = p1.y;
return new Vec2(
ax * t3 + bx * t2 + cx * t + dx,
ay * t3 + by * t2 + cy * t + dy,
);
}