@@ -1,10 +1,10 @@
< template >
< view class = "container" id = "pixi-box" ref = "pixiContainerRef" : style = "'transform:scale(' + scale + ')'" > < / view >
< view class = "container" id = "pixi-box" ref = "pixiContainerRef" > < / view >
< / template >
< script setup >
import { onMounted , onUnmounted , ref , nextTick } from ' vue' ;
const emit = defineEmits ( [ ' tag-click' ] ) ;
import { onMounted , onUnmounted , ref , nextTick } from " vue" ;
const emit = defineEmits ( [ " tag-click" ] ) ;
// DOM Ref
const pixiContainerRef = ref ( null ) ;
@@ -14,360 +14,339 @@ let app = null;
let tagsContainer = null ;
let activeTagInstances = [ ] ;
const scale = ref ( 0 )
// 配置数据
const mockTags = [
{ name : ' 医生' , bgColor : 0x0069fe , fontColor : 0xffffff , size : 17 , opacity : 1.0 , angle : 0 , radius : 0 } ,
{
name : ' 工程师' ,
bgColor : 0x87e2ec ,
fontColor : 0xffffff ,
size : 14 ,
opacity : 1 ,
angle : - Math . PI / 2 ,
radius : 68 ,
tailRotation : Math . PI / 2 ,
} ,
{
name : ' 建筑师' ,
bgColor : 0xffebeb ,
tailColor : 0xffe1e1 ,
fontColor : 0xff6969 ,
size : 11.5 ,
opacity : 1 ,
angle : - Math . PI / 4.2 ,
radius : 125 ,
tailRotation : ( 3 * Math . PI ) / 4 ,
} ,
{
name : ' 律师' ,
bgColor : 0x21ea85 ,
fontColor : 0xffffff ,
size : 15 ,
opacity : 1 ,
angle : - Math . PI / 10 ,
radius : 130 ,
tailRotation : ( 3 * Math . PI ) / 4 ,
} ,
{
name : ' 记者' ,
bgColor : 0xebf3ff ,
tailColor : 0xb9d3ff ,
fontColor : 0x1d71ef ,
size : 12 ,
opacity : 1 ,
angle : Math . PI / 120 ,
radius : 130 ,
tailRotation : ( 3 * Math . PI ) / 3.4 ,
} ,
{
name : ' 程序员' ,
bgColor : 0xffd4b6 ,
fontColor : 0xffffff ,
size : 14 ,
opacity : 1 ,
angle : Math . PI / 7 ,
radius : 120 ,
tailRotation : ( 5 * Math . PI ) / 4 ,
} ,
{
name : ' 摄影师' ,
bgColor : 0xd8e5fe ,
tailColor : 0xb9d3ff ,
fontColor : 0x1d71ef ,
size : 11 ,
opacity : 1 ,
angle : Math . PI / 3 ,
radius : 79 ,
tailRotation : ( 3 * Math . PI ) / 2 ,
} ,
{
name : ' 设计师' ,
bgColor : 0xff9400 ,
fontColor : 0xffffff ,
size : 14 ,
opacity : 1 ,
angle : ( 2 * Math . PI ) / 3 ,
radius : 92 ,
tailRotation : ( 7 * Math . PI ) / 4 ,
} ,
{
name : ' 心理咨询师' ,
bgColor : 0xebf3ff ,
tailColor : 0xb9d3ff ,
fontColor : 0x1d71ef ,
size : 10.5 ,
opacity : 1 ,
angle : ( 5.4 * Math . PI ) / 6 ,
radius : 110 ,
tailRotation : ( 3 * Math . PI ) / 1.78 ,
} ,
{
name : ' 护士' ,
bgColor : 0xff6969 ,
fontColor : 0xffffff ,
size : 15 ,
opacity : 1 ,
angle : ( 6.3 * Math . PI ) / 5.9 ,
radius : 110 ,
tailRotation : Math . PI / 4 ,
} ,
{
name : ' 会计' ,
bgColor : 0xfce9c9 ,
fontColor : 0xfbc55f ,
size : 13 ,
opacity : 1 ,
angle : ( 7.2 * Math . PI ) / 5.9 ,
radius : 120 ,
tailRotation : Math . PI / 4 ,
} ,
{ name : " 医生" , bgColor : 0x0069fe , fontColor : 0xffffff , size : 17 , opacity : 1.0 , angle : 0 , radius : 0 } ,
{
name : " 工程师" ,
bgColor : 0x87e2ec ,
fontColor : 0xffffff ,
size : 14 ,
opacity : 1 ,
angle : - Math . PI / 2 ,
radius : 68 ,
tailRotation : Math . PI / 2 ,
} ,
{
name : " 建筑师" ,
bgColor : 0xffebeb ,
tailColor : 0xffe1e1 ,
fontColor : 0xff6969 ,
size : 11.5 ,
opacity : 1 ,
angle : - Math . PI / 4.2 ,
radius : 125 ,
tailRotation : ( 3 * Math . PI ) / 4 ,
} ,
{
name : " 律师" ,
bgColor : 0x21ea85 ,
fontColor : 0xffffff ,
size : 15 ,
opacity : 1 ,
angle : - Math . PI / 10 ,
radius : 130 ,
tailRotation : ( 3 * Math . PI ) / 4 ,
} ,
{
name : " 记者" ,
bgColor : 0xebf3ff ,
tailColor : 0xb9d3ff ,
fontColor : 0x1d71ef ,
size : 12 ,
opacity : 1 ,
angle : Math . PI / 120 ,
radius : 130 ,
tailRotation : ( 3 * Math . PI ) / 3.4 ,
} ,
{
name : " 程序员" ,
bgColor : 0xffd4b6 ,
fontColor : 0xffffff ,
size : 14 ,
opacity : 1 ,
angle : Math . PI / 7 ,
radius : 120 ,
tailRotation : ( 5 * Math . PI ) / 4 ,
} ,
{
name : " 摄影师" ,
bgColor : 0xd8e5fe ,
tailColor : 0xb9d3ff ,
fontColor : 0x1d71ef ,
size : 11 ,
opacity : 1 ,
angle : Math . PI / 3 ,
radius : 79 ,
tailRotation : ( 3 * Math . PI ) / 2 ,
} ,
{
name : " 设计师" ,
bgColor : 0xff9400 ,
fontColor : 0xffffff ,
size : 14 ,
opacity : 1 ,
angle : ( 2 * Math . PI ) / 3 ,
radius : 92 ,
tailRotation : ( 7 * Math . PI ) / 4 ,
} ,
{
name : " 心理咨询师" ,
bgColor : 0xebf3ff ,
tailColor : 0xb9d3ff ,
fontColor : 0x1d71ef ,
size : 10.5 ,
opacity : 1 ,
angle : ( 5.4 * Math . PI ) / 6 ,
radius : 110 ,
tailRotation : ( 3 * Math . PI ) / 1.78 ,
} ,
{
name : " 护士" ,
bgColor : 0xff6969 ,
fontColor : 0xffffff ,
size : 15 ,
opacity : 1 ,
angle : ( 6.3 * Math . PI ) / 5.9 ,
radius : 110 ,
tailRotation : Math . PI / 4 ,
} ,
{
name : " 会计" ,
bgColor : 0xfce9c9 ,
fontColor : 0xfbc55f ,
size : 13 ,
opacity : 1 ,
angle : ( 7.2 * Math . PI ) / 5.9 ,
radius : 120 ,
tailRotation : Math . PI / 4 ,
} ,
] ;
onMounted ( async ( ) => {
await nextTick ( ) ;
setTimeout ( ( ) => {
initPixi ( ) ;
} , 100 ) ;
window . addEventListener ( ' resize' , handleResize ) ;
await nextTick ( ) ;
setTimeout ( ( ) => {
initPixi ( ) ;
} , 100 ) ;
window . addEventListener ( " resize" , handleResize ) ;
} ) ;
onUnmounted ( ( ) => {
window . removeEventListener ( ' resize' , handleResize ) ;
if ( app ) {
app . destroy ( true , { children : true , texture : true , baseTexture : true } ) ;
app = null ;
}
window . removeEventListener ( " resize" , handleResize ) ;
if ( app ) {
app . destroy ( true , { children : true , texture : true , baseTexture : true } ) ;
app = null ;
}
} ) ;
const getContainerDOM = ( ) => {
const refVal = pixiContainerRef . value ;
if ( ! refVal ) return document . getElementById ( ' pixi-box' ) ;
if ( refVal . $el ) return refVal . $el ;
return refVal ;
const refVal = pixiContainerRef . value ;
if ( ! refVal ) return document . getElementById ( " pixi-box" ) ;
if ( refVal . $el ) return refVal . $el ;
return refVal ;
} ;
const clamp = ( num , min , max ) => Math . min ( Math . max ( num , min ) , max ) ;
const initPixi = ( ) => {
const container = getContainerDOM ( ) ;
if ( ! container ) return ;
scale . value = Math . min ( container . clientWidth / 300 , 1.5 )
const width = container . clientWidth || 300 ;
const height = container . clientHeight || 300 ;
const container = getContainerDOM ( ) ;
if ( ! container ) return ;
if ( app ) return ;
const width = container . clientWidth || 300 ;
const height = container . clientHeight || 300 ;
app = new PIXI . Application ( {
width : width ,
height : height ,
backgroundAlpha : 0 ,
backgroundColor : 0xf5f7fa ,
antialias : true ,
resolution : window . devicePixelRatio || 1 ,
autoDensity : true ,
} ) ;
app . view . style . touchAction = 'auto' ;
if ( app ) return ;
container . a ppendChild ( app . view ) ;
app = new PIXI . A pplication ( {
width : width ,
height : height ,
backgroundAlpha : 0 ,
backgroundColor : 0xf5f7fa ,
antialias : true ,
resolution : window . devicePixelRatio || 1 ,
autoDensity : true ,
} ) ;
app . view . style . touchAction = "auto" ;
tagsC ontainer = new PIXI . Container ( ) ;
app . stage . addChild ( tagsContainer ) ;
c ontainer. appendChild ( app . view ) ;
renderScene ( width , height ) ;
tagsContainer = new PIXI . Container ( ) ;
app . stage . addChild ( tagsContainer ) ;
renderScene ( width , height ) ;
} ;
const renderScene = ( sw , sh ) => {
tagsContainer . removeChildren ( ) ;
activeTagInstances = [ ] ;
let ratio = window . innerWidth / 400 ;
if ( ratio < 1 ) ratio = 1 ;
tagsContainer . removeChildren ( ) ;
activeTagInstances = [ ] ;
const baseSize = 375 ;
const scaleFactor = ( M ath . min ( sw , sh ) / baseSize ) * 0.9 ;
mockTags . forEach ( ( data , index ) => {
const scaledRadius = d ata . radius * ratio ;
mockTags . forEach ( ( data , index ) => {
const scaledRadius = d ata . radius * ( scaleFactor < 1 ? 1 : scaleFactor * 0.8 ) ;
let x = sw / 2 + scaledRadius * Math . cos ( data . angle ) ;
let y = sh / 2 + scaledRadius * M ath . sin ( data . angle ) ;
le t x = sw / 2 + scaledRadius * Math . cos ( data . angle ) ;
let y = sh / 2 + scaledRadius * Math . sin ( data . angle ) ;
cons t tag = createTag ( data , index , ratio ) ;
const tag = createTag ( data , index ) ;
tagsContainer . addChild ( tag ) ;
tagsContainer . addChild ( tag ) ;
const safeW = tag . width / 2 + 10 ;
const safeH = tag . height / 2 + 10 ;
const safeW = tag . width / 2 + 10 ;
const safeH = tag . height / 2 + 10 ;
// 强制修正 x 和 y, 使其不超出屏幕
x = clamp ( x , safeW , sw - safeW ) ;
y = clamp ( y , safeH , sh - safeH ) ;
// 强制修正 x 和 y, 使其不超出屏幕
x = clamp ( x , safeW , sw - safeW ) ;
y = clamp ( y , safeH , sh - safeH ) ;
tag . x = x ;
tag . y = y ;
tag . x = x ;
tag . y = y ;
// 4. 保存元数据
tag . userData = {
originalX : x ,
originalY : y ,
angle : d ata . angle ,
radius : scaledRadius ,
floatOffset : Math . random ( ) * Math . PI * 2 ,
floatSpeed : 0.01 + Math . random ( ) * 0.02 ,
floatRange : 2 + Math . random ( ) * 2 ,
safeH : safeH ,
} ;
if ( data . radius > 0 ) {
const tail = createCometTail ( data . tailColor || data . bgColor , data . tailRotation , tag . width ) ;
tag . addChildAt ( tail , 0 ) ;
tag . updateTail = ( ) => tail . updateAnim ( ) ;
}
activeTagInstances . push ( tag ) ;
} ) ;
// 动画循环
app . ticker . add ( ( ) => {
const screenH = app . screen . height ;
activeTagInstances . forEach ( ( tag ) => {
const meta = tag . userData ;
if ( meta ) {
// 计算新的浮动位置
meta . floatOffset += meta . floatSpeed ;
let nextY = meta . originalY + Math . sin ( meta . floatOffset ) * meta . floatRange ;
// 再次进行边界检查
if ( nextY < meta . safeH ) nextY = meta . safeH ;
if ( nextY > screenH - meta . safeH ) nextY = screenH - meta . safeH ;
tag . y = nextY ;
if ( tag . updateTail ) tag . updateTail ( ) ;
}
} ) ;
} ) ;
} ;
const createTag = ( tagData , index ) => {
const tagGroup = new PIXI . Container ( ) ;
tagGroup . eventMode = 'static' ;
tagGroup . cursor = 'pointer' ;
tagGroup . on ( 'pointertap' , ( ) => emit ( 'tag-click' , tagData ) ) ;
const text = new PIXI . Text ( tagData . name , {
fontFamily : [ 'PingFang SC' , 'Microsoft YaHei' , 'Arial' ] ,
fontSize : tagData . size ,
fill : tagData . fontColor ,
padding : 4 ,
resolution : 2 ,
} ) ;
text . anchor . set ( 0.5 ) ;
const paddingH = 26 ;
const paddingV = 10 ;
let bgWidth = text . width + paddingH ;
let bgHeight = text . height + paddingV ;
if ( index === 0 ) bgWidth = Math . max ( bgWidth , tagData . size * 4.5 ) ;
const bg = new PIXI . Graphics ( ) ;
bg . beginFill ( tagData . bgColor , tagData . opacity ? ? 1 ) ;
bg . drawRoundedRect ( - bgWidth / 2 , - bgHeight / 2 , bgWidth , bgHeight , bgHeight / 2 ) ;
bg . endFill ( ) ;
tagGroup . addChild ( bg ) ;
tagGroup . addChild ( text ) ;
return tagGroup ;
} ;
const createCometTail = ( bgColor , tailRotation , parentWidth ) => {
const tailGroup = new PIXI . Container ( ) ;
const graphics = new PIXI . Graphics ( ) ;
tailGroup . addChild ( graphics ) ;
const baseLength = 45 ;
const startWidth = parentWidth * 0.6 ;
const endWidth = 20 ;
let breathPhase = Math . random ( ) * Math . PI * 2 ;
const breathSpeed = 0.04 ;
tailGroup . updateAnim = ( ) => {
breathPhase += breathSpeed ;
const breathScale = 0.85 + 0.15 * Math . sin ( breathPhase ) ;
graphics . clear ( ) ;
const currentLength = baseLength * breathScale ;
const cos = Math . cos ( tailRotation ) ;
const sin = Math . sin ( tailRotation ) ;
const perpX = - sin ;
const perpY = cos ;
const p1 = { x : perpX * ( startWidth / 2 ) , y : perpY * ( startWidth / 2 ) } ;
const p2 = { x : - perpX * ( startWidth / 2 ) , y : - perpY * ( startWidth / 2 ) } ;
const endCX = cos * currentLength ;
const endCY = sin * currentLength ;
const p3 = { x : endCX - perpX * ( endWidth / 2 ) , y : endCY - perpY * ( endWidth / 2 ) } ;
const p4 = { x : endCX + perpX * ( endWidth / 2 ) , y : endCY + perpY * ( endWidth / 2 ) } ;
const segments = 8 ;
for ( let i = 0 ; i < segments ; i ++ ) {
const t1 = i / segments ;
const t2 = ( i + 1 ) / segments ;
const alpha = 0.4 * ( 1 - t1 ) ;
const sp1 = { x : p1 . x + ( p4 . x - p1 . x ) * t1 , y : p1 . y + ( p4 . y - p1 . y ) * t1 } ;
const sp2 = { x : p2 . x + ( p3 . x - p2 . x ) * t1 , y : p2 . y + ( p3 . y - p2 . y ) * t1 } ;
const ep1 = { x : p1 . x + ( p4 . x - p1 . x ) * t2 , y : p1 . y + ( p4 . y - p1 . y ) * t2 } ;
const ep2 = { x : p2 . x + ( p3 . x - p2 . x ) * t2 , y : p2 . y + ( p3 . y - p2 . y ) * t2 } ;
graphics . beginFill ( bgColor , alpha ) ;
graphics . moveTo ( sp1 . x , sp1 . y ) ;
graphics . lineTo ( sp2 . x , sp2 . y ) ;
graphics . lineTo ( ep2 . x , ep2 . y ) ;
graphics . lineTo ( ep1 . x , ep1 . y ) ;
graphics . endFill ( ) ;
}
// 4. 保存元数据
tag . userData = {
originalX : x ,
originalY : y ,
angle : da ta. angle ,
radius : scaledRadius ,
floatOffset : Math . random ( ) * Math . PI * 2 ,
floatSpeed : 0.01 + M ath . r andom ( ) * 0.02 ,
floatRange : 2 + Math . random ( ) * 2 ,
safeH : safeH ,
} ;
tailGroup . updateAnim ( ) ;
return tailGroup ;
if ( data . radius > 0 ) {
const tail = createCometTail ( data . tailColor || data . bgColor , data . tailRotation , tag . width , ratio ) ;
tag . addChildAt ( tail , 0 ) ;
tag . updateTail = ( ) => tail . updateAnim ( ) ;
}
activeTagInstances . push ( tag ) ;
} ) ;
// 动画循环
app . ticker . add ( ( ) => {
const screenH = app . screen . height ;
activeTagInstances . forEach ( ( tag ) => {
const meta = tag . userData ;
if ( meta ) {
// 计算新的浮动位置
meta . floatOffset += meta . floatSpeed ;
let nextY = meta . originalY + Math . sin ( meta . floatOffset ) * meta . floatRange ;
// 再次进行边界检查
if ( nextY < meta . safeH ) nextY = meta . safeH ;
if ( nextY > screenH - meta . safeH ) nextY = screenH - meta . safeH ;
tag . y = nextY ;
if ( tag . updateTail ) tag . updateTail ( ) ;
}
} ) ;
} ) ;
} ;
const createTag = ( tagData , index , ratio ) => {
if ( ratio > 1 ) ratio = ratio * 0.9 ;
const tagGroup = new PIXI . Container ( ) ;
tagGroup . eventMode = "static" ;
tagGroup . cursor = "pointer" ;
tagGroup . on ( "pointertap" , ( ) => emit ( "tag-click" , tagData ) ) ;
const text = new PIXI . Text ( tagData . name , {
fontFamily : [ "PingFang SC" , "Microsoft YaHei" , "Arial" ] ,
fontSize : tagData . size * ratio ,
fill : tagData . fontColor ,
padding : 4 * ratio ,
resolution : 2 ,
} ) ;
text . anchor . set ( 0.5 ) ;
const paddingH = 26 * ratio ;
const paddingV = 10 * ratio ;
let bgWidth = text . width + paddingH ;
let bgHeight = text . height + paddingV ;
if ( index === 0 ) bgWidth = Math . max ( bgWidth , tagData . size * 4.5 ) ;
const bg = new PIXI . Graphics ( ) ;
bg . beginFill ( tagData . bgColor , tagData . opacity ? ? 1 ) ;
bg . drawRoundedRect ( - bgWidth / 2 , - bgHeight / 2 , bgWidth , bgHeight , bgHeight / 2 ) ;
bg . endFill ( ) ;
tagGroup . addChild ( bg ) ;
tagGroup . addChild ( text ) ;
return tagGroup ;
} ;
const createCometTail = ( bgColor , tailRotation , parentWidth , ratio ) => {
if ( ratio > 1 ) ratio = ratio * 0.9 ;
const tailGroup = new PIXI . Container ( ) ;
const graphics = new PIXI . Graphics ( ) ;
tailGroup . addChild ( graphics ) ;
const baseLength = 45 * ratio ;
const startWidth = parentWidth * 0.6 ;
const endWidth = 20 * ratio ;
let breathPhase = Math . random ( ) * Math . PI * 2 ;
const breathSpeed = 0.04 ;
tailGroup . updateAnim = ( ) => {
breathPhase += breathSpeed ;
const breathScale = 0.85 + 0.15 * Math . sin ( breathPhase ) ;
graphics . clear ( ) ;
const currentLength = baseLength * breathScale ;
const cos = Math . cos ( tailRotation ) ;
const sin = Math . sin ( tailRotation ) ;
const perpX = - sin ;
const perpY = cos ;
const p1 = { x : perpX * ( startWidth / 2 ) , y : perpY * ( startWidth / 2 ) } ;
const p2 = { x : - perpX * ( startWidth / 2 ) , y : - perpY * ( startWidth / 2 ) } ;
const endCX = cos * currentLength ;
const endCY = sin * currentLength ;
const p3 = { x : endCX - perpX * ( endWidth / 2 ) , y : endCY - perpY * ( endWidth / 2 ) } ;
const p4 = { x : endCX + perpX * ( endWidth / 2 ) , y : endCY + perpY * ( endWidth / 2 ) } ;
const segments = 8 ;
for ( let i = 0 ; i < segments ; i ++ ) {
const t1 = i / segments ;
const t2 = ( i + 1 ) / segments ;
const alpha = 0.4 * ( 1 - t1 ) ;
const sp1 = { x : p1 . x + ( p4 . x - p1 . x ) * t1 , y : p1 . y + ( p4 . y - p1 . y ) * t1 } ;
const sp2 = { x : p2 . x + ( p3 . x - p2 . x ) * t1 , y : p2 . y + ( p3 . y - p2 . y ) * t1 } ;
const ep1 = { x : p1 . x + ( p4 . x - p1 . x ) * t2 , y : p1 . y + ( p4 . y - p1 . y ) * t2 } ;
const ep2 = { x : p2 . x + ( p3 . x - p2 . x ) * t2 , y : p2 . y + ( p3 . y - p2 . y ) * t2 } ;
graphics . beginFill ( bgColor , alpha ) ;
graphics . moveTo ( sp1 . x , sp1 . y ) ;
graphics . lineTo ( sp2 . x , sp2 . y ) ;
graphics . lineTo ( ep2 . x , ep2 . y ) ;
graphics . lineTo ( ep1 . x , ep1 . y ) ;
graphics . endFill ( ) ;
}
} ;
tailGroup . updateAnim ( ) ;
return tailGroup ;
} ;
const handleResize = ( ) => {
const container = getContainerDOM ( ) ;
if ( ! app || ! container ) return ;
const w = container . clientWidth || 300 ;
const h = container . clientHeight || 300 ;
app . renderer . resize ( w , h ) ;
activeTagInstances . forEach ( ( tag ) => {
const meta = tag . userData ;
if ( ! meta ) return ;
let newX = w / 2 + meta . radius * Math . cos ( meta . angle ) ;
let newY = h / 2 + meta . radius * Math . sin ( meta . angle ) ;
const safeW = tag . width / 2 + 10 ;
const safeH = tag . height / 2 + 10 ;
meta . originalX = clamp ( newX , safeW , w - safeW ) ;
meta . originalY = clamp ( newY , safeH , h - safeH ) ;
meta . safeH = safeH ; // 更新安全高度
tag . x = meta . originalX ;
} ) ;
const container = getContainerDOM ( ) ;
if ( ! app || ! container ) return ;
app . destroy ( true , { children : true , texture : true , baseTexture : true } ) ;
app = null ;
initPixi ( )
} ;
< / script >
< style scoped >
. container {
width : 100 % ;
height : 5 00rpx ;
position : relative ;
overflow : hidden ;
color : # b9d3ff ;
width : 100 % ;
height : 1 00% ;
position : relative ;
overflow : hidden ;
color : # b9d3ff ;
}
< / style >