feat:去掉无用的函数方法

This commit is contained in:
zerosaturation 2026-07-09 16:08:25 +08:00
parent fbf3e6c63c
commit 31eee930ca
13 changed files with 1712 additions and 2058 deletions

151
dify-laser-workflow.drawio Normal file
View File

@ -0,0 +1,151 @@
<mxfile host="app.diagrams.net" modified="2026-07-08T10:00:00.000Z" agent="Claude" version="22.0.0" type="device">
<diagram name="Dify Laser Workflow v3" id="laser-workflow-v3-with-compositor">
<mxGraphModel dx="2400" dy="1500" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2000" pageHeight="1400" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title" value="镭射卡生成完整链路 v3含后端 compositor 合成)&#10;AI 出金属底 + 后端合成人像 + 镭射光效" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="500" y="20" width="1000" height="60" as="geometry" />
</mxCell>
<mxCell id="phase_dify" value="=== Dify 工作流laser_card_v2_5series===" style="text;html=1;strokeColor=#9673a6;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="40" y="100" width="1920" height="30" as="geometry" />
</mxCell>
<mxCell id="start" value="开始节点 (Start)&#10;输入变量:&#10;- cutout_url (string, 必填)&#10;&#10;用户风格描述词由 Dify 原生 #context# 注入&#10;(不添加自定义变量)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11" vertex="1" parent="1">
<mxGeometry x="120" y="140" width="320" height="100" as="geometry" />
</mxCell>
<mxCell id="llm" value="LLM 节点 (gpt-4o-mini)&#10;&#10;系统 promptspec §5.2 中文版&#10;5 系列模板 + 5 字段 JSON Schema&#10;&#10;用户提示:#context#&#10;输出格式JSON结构化输出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="60" y="280" width="440" height="120" as="geometry" />
</mxCell>
<mxCell id="branch_label" value="5 路并发(每路对应一个卡牌系列)&#10;AI 出『金属底 + 风格氛围』背景图(纯文生图,不带人像)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;fontStyle=2" vertex="1" parent="1">
<mxGeometry x="60" y="420" width="440" height="30" as="geometry" />
</mxCell>
<mxCell id="p1_gen" value="生图节点 1&#10;gen_prizm_blue&#10;&#10;模型image-01-live&#10;Prompt{{ llm_rewrite.prizm_blue }}&#10;尺寸1024x1024&#10;数量1&#10;&#10;→ 金属底背景图 1Prizm Blue 工艺)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="40" y="470" width="180" height="140" as="geometry" />
</mxCell>
<mxCell id="p2_gen" value="生图节点 2&#10;gen_pokemon_rainbow&#10;&#10;Prompt{{ llm_rewrite.pokemon_rainbow }}&#10;→ 金属底背景图 2彩虹稀有" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="240" y="470" width="180" height="140" as="geometry" />
</mxCell>
<mxCell id="p3_gen" value="生图节点 3&#10;gen_topps_refractor&#10;&#10;Prompt{{ llm_rewrite.topps_refractor }}&#10;→ 金属底背景图 3Topps 折射)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="440" y="470" width="180" height="140" as="geometry" />
</mxCell>
<mxCell id="p4_gen" value="生图节点 4&#10;gen_yugioh_secret&#10;&#10;Prompt{{ llm_rewrite.yugioh_secret }}&#10;→ 金属底背景图 4横纹 Secret" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="640" y="470" width="180" height="140" as="geometry" />
</mxCell>
<mxCell id="p5_gen" value="生图节点 5&#10;gen_ws_ssp&#10;&#10;Prompt{{ llm_rewrite.ws_ssp }}&#10;→ 金属底背景图 5WS SSP" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="840" y="470" width="180" height="140" as="geometry" />
</mxCell>
<mxCell id="end_dify" value="Dify 结束节点&#10;&#10;输出 variants[5]&#10;每项 = {preset_id, signed_url(AI背景图), oss_key}&#10;&#10;⚠️ 注意signed_url 是 AI 出的金属底背景图,&#10;不含人像(人像在后端 compositor 叠加)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="60" y="640" width="960" height="80" as="geometry" />
</mxCell>
<mxCell id="phase_backend" value="=== 后端 gateway 服务topfans-gateway===" style="text;html=1;strokeColor=#d6b656;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="40" y="760" width="1920" height="30" as="geometry" />
</mxCell>
<mxCell id="handle_dify" value="handleDifyBlocking() 控制器&#10;&#10;1. POST /v1/workflows/run 调 Dify&#10;2. 解析 5 个 variantAI 背景图)&#10;3. **逐张调 compositor.Compose() 合成**&#10;4. 上传合成图到 OSS&#10;5. 返回 5 张最终镭射卡" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="60" y="800" width="380" height="160" as="geometry" />
</mxCell>
<mxCell id="compositor" value="compositor.Compose()【核心6 层合成】&#10;&#10;输入:&#10; - BackgroundURLAI 出的金属底背景图)&#10; - CutoutURL用户上传的人像&#10; - GratingConfig5 套光栅参数)&#10;&#10;合成 6 层:&#10; 1. AI 艺术背景&#10; 2. 金属反光梯度&#10; 3. 彩虹衍射光栅色带(核心镭射)&#10; 4. AI 装饰层(可选)&#10; 5. 胶片颗粒 + 磨砂哑光&#10; 6. 人物抠图(最顶层)&#10;&#10;输出:合成后的 PNG bytes" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fad7ac;strokeColor=#b46504;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="500" y="800" width="520" height="280" as="geometry" />
</mxCell>
<mxCell id="oss" value="OSS 上传&#10;&#10;把合成 PNG bytes&#10;上传到 top-fans-test bucket&#10;key: laser-card/compositor/&#10;{star_id}/{user_id}/{preset_id}_&#10;{ts}.png&#10;&#10;返回 final_urlsigned URL2h 过期)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="1080" y="800" width="380" height="160" as="geometry" />
</mxCell>
<mxCell id="frontend" value="前端 LenticularCard.vue&#10;&#10;5 张最终镭射卡&#10;(每张都是『用户人像 + 镭射金属底 + 风格场景』完整合成)&#10;&#10;用户选 1 张 → 走铸造" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="1500" y="800" width="460" height="160" as="geometry" />
</mxCell>
<mxCell id="phase_db" value="=== 数据库topfans-postgres===" style="text;html=1;strokeColor=#82b366;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="40" y="1130" width="1920" height="30" as="geometry" />
</mxCell>
<mxCell id="db" value="laser_card_instances 表&#10;&#10;INSERT 一行:&#10; - instance_no (LC + 时间戳 + 随机)&#10; - template_id (preset_id → template.id)&#10; - status = &#39;rendered&#39;&#10; - materials_snapshot = [{role:&#39;composite&#39;, oss_key, preset_id} x5]&#10; - composite_oss_key = final image key" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;align=left;spacingLeft=10" vertex="1" parent="1">
<mxGeometry x="60" y="1170" width="800" height="160" as="geometry" />
</mxCell>
<mxCell id="legend" value="图例:&#10;■ 绿色 = 开始 / 输入 / 前端 / DB&#10;■ 蓝色 = LLM 处理 / DB 操作&#10;■ 紫色 = AI 生图节点5 路并发)&#10;■ 黄色 = 后端 gateway 处理&#10;■ 橙色 = compositor 合成6 层)&#10;■ 红色 = 结束 / 输出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=11;align=left;spacingLeft=10;verticalAlign=top" vertex="1" parent="1">
<mxGeometry x="1500" y="1170" width="460" height="160" as="geometry" />
</mxCell>
<mxCell id="e_start_llm" value="cutout_url + #context#" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;endArrow=classic;strokeColor=#6c8ebf;strokeWidth=2;fontSize=11" edge="1" parent="1" source="start" target="llm">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_llm_p1" value="json.prizm_blue" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.1;exitY=1;entryX=0.5;entryY=0;endArrow=classic;strokeColor=#9673a6;strokeWidth=1;fontSize=11" edge="1" parent="1" source="llm" target="p1_gen">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_llm_p2" value="json.pokemon_rainbow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.3;exitY=1;entryX=0.5;entryY=0;endArrow=classic;strokeColor=#9673a6;strokeWidth=1;fontSize=11" edge="1" parent="1" source="llm" target="p2_gen">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_llm_p3" value="json.topps_refractor" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;endArrow=classic;strokeColor=#9673a6;strokeWidth=1;fontSize=11" edge="1" parent="1" source="llm" target="p3_gen">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_llm_p4" value="json.yugioh_secret" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.7;exitY=1;entryX=0.5;entryY=0;endArrow=classic;strokeColor=#9673a6;strokeWidth=1;fontSize=11" edge="1" parent="1" source="llm" target="p4_gen">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_llm_p5" value="json.ws_ssp" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.9;exitY=1;entryX=0.5;entryY=0;endArrow=classic;strokeColor=#9673a6;strokeWidth=1;fontSize=11" edge="1" parent="1" source="llm" target="p5_gen">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_p1_end" value="bg1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.1;entryY=0;endArrow=classic;strokeColor=#b85450;strokeWidth=1;fontSize=10" edge="1" parent="1" source="p1_gen" target="end_dify">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_p2_end" value="bg2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.25;entryY=0;endArrow=classic;strokeColor=#b85450;strokeWidth=1;fontSize=10" edge="1" parent="1" source="p2_gen" target="end_dify">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_p3_end" value="bg3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.4;entryY=0;endArrow=classic;strokeColor=#b85450;strokeWidth=1;fontSize=10" edge="1" parent="1" source="p3_gen" target="end_dify">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_p4_end" value="bg4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.55;entryY=0;endArrow=classic;strokeColor=#b85450;strokeWidth=1;fontSize=10" edge="1" parent="1" source="p4_gen" target="end_dify">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_p5_end" value="bg5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.7;entryY=0;endArrow=classic;strokeColor=#b85450;strokeWidth=1;fontSize=10" edge="1" parent="1" source="p5_gen" target="end_dify">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_end_backend" value="variants[5]" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;endArrow=classic;strokeColor=#d6b656;strokeWidth=2;fontSize=12" edge="1" parent="1" source="end_dify" target="handle_dify">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_handle_compositor" value="5x compose call" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=1;exitY=0.5;entryX=0;entryY=0.5;endArrow=classic;strokeColor=#b46504;strokeWidth=2;fontSize=12" edge="1" parent="1" source="handle_dify" target="compositor">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_compositor_oss" value="PNG bytes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=1;exitY=0.5;entryX=0;entryY=0.5;endArrow=classic;strokeColor=#d6b656;strokeWidth=2;fontSize=12" edge="1" parent="1" source="compositor" target="oss">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_oss_frontend" value="5 final URLs" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=1;exitY=0.5;entryX=0;entryY=0.5;endArrow=classic;strokeColor=#82b366;strokeWidth=2;fontSize=12" edge="1" parent="1" source="oss" target="frontend">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_handle_dify" value="INSERT" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;endArrow=classic;strokeColor=#6c8ebf;strokeWidth=1;fontSize=11" edge="1" parent="1" source="handle_dify" target="db">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -159,22 +159,24 @@ do_install() {
print_msg "$YELLOW" " 如需重新配置,请先备份 ${SERVER_PATH}/.env 后删除"
else
# 下载 Dify docker-compose
print_msg "$YELLOW" "下载 Dify docker-compose..."
# ★ 版本说明v0.15.0 的 sandbox 镜像有 seccomp bugPython 启动崩溃)
# 必须用 Dify v1.x 或更新版本
print_msg "$YELLOW" "下载 Dify docker-composev1.x..."
ssh_cmd "cd ${SERVER_PATH} && \
curl -L https://raw.githubusercontent.com/langgenius/dify/main/docker/docker-compose.yaml \
-o docker-compose.yml 2>/dev/null || \
curl -L https://raw.githubusercontent.com/langgenius/dify/v0.15.0/docker/docker-compose.yaml \
curl -L https://raw.githubusercontent.com/langgenius/dify/1.4.0/docker/docker-compose.yaml \
-o docker-compose.yml"
print_msg "$GREEN" "✅ docker-compose.yml 下载完成"
print_msg "$GREEN" "✅ docker-compose.yml 下载完成v1.x"
# 下载 .env 示例文件
print_msg "$YELLOW" "下载 .env 配置..."
print_msg "$YELLOW" "下载 .env 配置v1.x..."
ssh_cmd "cd ${SERVER_PATH} && \
curl -L https://raw.githubusercontent.com/langgenius/dify/main/docker/.env.example \
-o .env 2>/dev/null || \
curl -L https://raw.githubusercontent.com/langgenius/dify/v0.15.0/docker/.env.example \
curl -L https://raw.githubusercontent.com/langgenius/dify/1.4.0/docker/.env.example \
-o .env"
print_msg "$GREEN" "✅ .env 下载完成"
print_msg "$GREEN" "✅ .env 下载完成v1.x"
# 修改 .env 配置(端口、密码等)
# ★ heredoc delimiter 不可加单引号,否则 \$VAR 不展开

File diff suppressed because it is too large Load Diff

View File

@ -1,57 +0,0 @@
<template>
<view class="lc-wrap">
<view class="lc-frame">
<LenticularCard
:layers="lenticularLayers"
:transforms="layerTransforms"
gyro-source="simulation"
:approximate-preview="true"
tilt-hint-text="滑动卡片预览光栅效果"
:show-slider="showSlider"
:simulate-tilt-from-normalized="simulate"
/>
</view>
</view>
</template>
<script setup>
import { ref, watch } from 'vue'
import LenticularCard from './LenticularCard.vue'
import { useLenticularPreview } from '@/composables/useLenticularPreview.js'
import { buildLenticularLayers } from '@/utils/castloveMintForm.js'
const props = defineProps({
/** 用户上传图:作为光栅主体层 */
imageSrc: {
type: String,
default: '',
},
showSlider: { type: Boolean, default: false },
})
const lenticularLayers = ref(buildLenticularLayers(props.imageSrc || ''))
watch(
() => props.imageSrc,
(s) => {
lenticularLayers.value = buildLenticularLayers(s || '')
}
)
const { layerTransforms, simulate } = useLenticularPreview(lenticularLayers)
</script>
<style scoped>
.lc-wrap {
width: 100%;
display: flex;
justify-content: center;
padding: 16rpx 0 32rpx;
}
.lc-frame {
width: 100%;
height: 520rpx;
max-height: 70vh;
}
</style>

View File

@ -1,277 +0,0 @@
<template>
<view class="holo-container">
<view class="holo-frame" :style="frameStyle">
<!-- WebGL 渲染层 Canvas 缓冲 = CSS × DPRMipmap + MSAA -->
<canvas
v-show="webglReady && !hasError"
ref="webglCanvas"
class="holo-canvas"
:style="canvasStyle"
@touchstart.stop="onTouchStart"
@touchmove.stop.prevent="onTouchMove"
@touchend.stop="onTouchEnd"
@touchcancel.stop="onTouchEnd"
/>
<!-- CSS 降级层WebGL 不可用时自动启用 -->
<view v-if="!webglReady || hasError" class="holo-fallback" :style="fallbackStyle">
<view class="holo-fallback-card" :style="fallbackCardStyle">
<image
v-if="baseImageSrc"
class="holo-fallback-img"
:src="baseImageSrc"
mode="aspectFill"
/>
<view class="holo-fallback-shimmer" />
<view class="holo-fallback-rim" />
</view>
</view>
<view v-if="showHint && hintText" class="holo-hint" :class="{ 'holo-hint--hidden': !showHint }">
<text class="holo-hint-icon"></text>
<text class="holo-hint-text">{{ hintText }}</text>
</view>
</view>
<ManualTiltSlider
v-if="showSlider"
:value="sliderValue"
@change="v => emit('update:sliderValue', v)"
@reset="emit('sliderReset')"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { HolographicEngine } from '@/utils/webgl/holographic-engine.js'
import { loadTextureImage } from '@/utils/laser-card/laserPreviewWebgl.js'
import ManualTiltSlider from './ManualTiltSlider.vue'
const props = defineProps({
baseImageSrc: { type: String, default: '' },
aspectRatio: { type: Number, default: 3 / 4 },
maxWidth: { type: Number, default: 420 },
cornerRadius: { type: Number, default: 24 },
effectIntensity: { type: Number, default: 0.85 },
dispersionStrength: { type: Number, default: 1.0 },
diffractionScale: { type: Number, default: 0.7 },
highlightSpeed: { type: Number, default: 0.8 },
highlightWidth: { type: Number, default: 1.0 },
fresnelPower: { type: Number, default: 3.5 },
noiseScale: { type: Number, default: 1.0 },
noiseOctaves: { type: Number, default: 6 },
safeZoneRadius: { type: Number, default: 0.35 },
safeZoneSoftness: { type: Number, default: 0.15 },
viewX: { type: Number, default: 0 },
viewY: { type: Number, default: 0 },
viewZ: { type: Number, default: 0 },
hintText: { type: String, default: '移动光标或倾斜设备' },
showHint: { type: Boolean, default: true },
maxDPR: { type: Number, default: 2 },
skipBuiltInTouch: { type: Boolean, default: false },
// LenticularCard
showSlider: { type: Boolean, default: false },
sliderValue: { type: Number, default: 0.5 },
})
const emit = defineEmits(['simulate', 'update:sliderValue', 'sliderReset', 'ready', 'error', 'fpsUpdate'])
const webglCanvas = ref(null)
const webglReady = ref(false)
const hasError = ref(false)
let engine = null
let resizeObserver = null
let fpsInterval = null
function resolveSize() {
const vw = typeof window !== 'undefined' ? window.innerWidth : 375
const w = Math.min(props.maxWidth, vw * 0.92)
const h = w / props.aspectRatio
return { w, h }
}
const frameStyle = computed(() => {
const { w, h } = resolveSize()
return { width: `${w}px`, height: `${h}px` }
})
const canvasStyle = computed(() => {
const { w, h } = resolveSize()
return { width: `${w}px`, height: `${h}px` }
})
const fallbackStyle = computed(() => {
const { w, h } = resolveSize()
return { width: `${w}px`, height: `${h}px` }
})
const fallbackCardStyle = computed(() => {
const vx = props.viewX || 0
const vy = props.viewY || 0
return {
borderRadius: `${props.cornerRadius}px`,
transform: `perspective(1000px) rotateY(${vx * 15}deg) rotateX(${-vy * 8}deg)`,
}
})
function dispatchTouch(clientX, clientY) {
if (props.skipBuiltInTouch) return
const canvas = webglCanvas.value
if (!canvas) return
const rect = canvas.getBoundingClientRect()
if (!rect.width || !rect.height) return
const cx = rect.left + rect.width / 2
const cy = rect.top + rect.height / 2
emit('simulate',
Math.max(-1, Math.min(1, (clientX - cx) / (rect.width / 2) * 1.1)),
Math.max(-1, Math.min(1, (clientY - cy) / (rect.height / 2) * 1.1))
)
}
function getT(e) {
if (e.touches && e.touches[0]) return e.touches[0]
if (e.changedTouches && e.changedTouches[0]) return e.changedTouches[0]
return null
}
function onTouchStart(e) { onTouchMove(e) }
function onTouchMove(e) {
if (props.skipBuiltInTouch) return
const t = getT(e)
if (t) dispatchTouch(t.clientX, t.clientY)
}
function onTouchEnd() {
if (props.skipBuiltInTouch) return
// emit('simulate', 0, 0)
// (0,0)
}
function syncEngineParams() {
if (!engine) return
engine.setEffectParams({
effectIntensity: props.effectIntensity,
dispersionStrength: props.dispersionStrength,
diffractionScale: props.diffractionScale,
highlightSpeed: props.highlightSpeed,
highlightWidth: props.highlightWidth,
fresnelPower: props.fresnelPower,
noiseScale: props.noiseScale,
noiseOctaves: Math.round(props.noiseOctaves),
cardCornerRadius: props.cornerRadius,
safeZoneRadius: props.safeZoneRadius,
safeZoneSoftness: props.safeZoneSoftness,
})
engine.setViewAngle(props.viewX, props.viewY, props.viewZ)
}
watch(() => [
props.effectIntensity, props.dispersionStrength, props.diffractionScale,
props.highlightSpeed, props.highlightWidth, props.fresnelPower,
props.noiseScale, props.noiseOctaves, props.cornerRadius,
props.safeZoneRadius, props.safeZoneSoftness,
props.viewX, props.viewY, props.viewZ,
], syncEngineParams)
watch(() => props.baseImageSrc, async (src) => {
if (!engine || !src) return
try {
const img = await loadTextureImage(src)
engine.uploadBaseImage(img)
} catch (e) {
console.warn('[HolographicCard] load baseImage failed:', e)
}
})
function initWebGL() {
if (!HolographicEngine.isSupported()) {
hasError.value = true
emit('error', new Error('WebGL not supported'))
return
}
const canvas = webglCanvas.value
if (!canvas) return
try {
const { w, h } = resolveSize()
engine = new HolographicEngine(canvas, {
alpha: true,
antialias: true,
useFBO: false,
})
engine.setDPR(Math.min(window.devicePixelRatio || 1, props.maxDPR))
if (!engine.init()) {
hasError.value = true
emit('error', new Error('WebGL init failed'))
return
}
syncEngineParams()
engine.start()
webglReady.value = true
emit('ready')
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver((entries) => {
for (const e of entries) {
const { width, height } = e.contentRect
if (engine && width > 0 && height > 0) engine.resize(width, height)
}
})
resizeObserver.observe(canvas.parentElement || canvas)
}
fpsInterval = setInterval(() => {
if (engine) emit('fpsUpdate', engine.getFPS())
}, 1000)
if (props.baseImageSrc) {
loadTextureImage(props.baseImageSrc)
.then(img => engine.uploadBaseImage(img))
.catch(e => console.warn('[HolographicCard] init load:', e))
}
} catch (e) {
console.error('[HolographicCard] init error:', e)
hasError.value = true
emit('error', e)
}
}
onMounted(() => { nextTick(initWebGL) })
onUnmounted(() => {
if (fpsInterval) { clearInterval(fpsInterval); fpsInterval = null }
if (resizeObserver) { resizeObserver.disconnect(); resizeObserver = null }
if (engine) { engine.destroy(); engine = null }
})
</script>
<style scoped>
.holo-container { width: 100%; display: flex; align-items: center; justify-content: center; }
.holo-frame { position: relative; max-width: 100%; }
.holo-canvas { display: block; border-radius: inherit; }
/* ---- CSS 降级 ---- */
.holo-fallback { position: relative; display: flex; align-items: center; justify-content: center; perspective: 1000px; }
.holo-fallback-card {
width: 100%; height: 100%; overflow: hidden; position: relative;
background-color: #060e20; box-shadow: 0 20px 50px rgba(0,0,0,0.55);
border: 1px solid rgba(255,255,255,0.18);
will-change: transform; transform-style: preserve-3d;
}
.holo-fallback-img { width: 100%; height: 100%; position: absolute; top: 0; left: 0; }
.holo-fallback-shimmer {
position: absolute; inset: 0; pointer-events: none;
background: linear-gradient(135deg, rgba(221,183,255,0.08) 0%, rgba(100,200,255,0.15) 25%, transparent 50%, rgba(255,180,220,0.12) 75%, rgba(76,215,246,0.08) 100%);
}
.holo-fallback-rim {
position: absolute; inset: 0;
border-top: 1px solid rgba(221,183,255,0.42);
box-shadow: inset 0 0 40px rgba(255,255,255,0.05);
pointer-events: none;
}
/* ---- 提示 ---- */
.holo-hint {
position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 6px;
padding: 8px 16px; background: rgba(0,0,0,0.55); backdrop-filter: blur(12px);
border-radius: 20px; transition: opacity 0.4s ease; pointer-events: none;
}
.holo-hint--hidden { opacity: 0; }
.holo-hint-icon { font-size: 16px; color: rgba(255,255,255,0.8); }
.holo-hint-text { font-size: 12px; color: rgba(255,255,255,0.75); }
</style>

View File

@ -1,76 +0,0 @@
/**
* 全息镭射卡预览组合式函数
*
* 集成 WebGL HolographicEngine + 触摸交互
* simulate(x, y) 是唯一驱动入口
*/
import { ref, reactive } from 'vue'
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)) }
function lerp(a, b, t) { return a + (b - a) * t }
export function useHolographicPreview() {
const physics = reactive({
tiltSensitivity: 72,
transitionSmoothness: 66,
angleStability: 88,
sensorDeadzoneStrength: 1,
gyroSimEnabled: true,
})
const viewAngle = reactive({ x: 0, y: 0, z: 0 })
const isWebGLReady = ref(false)
const hasWebGLError = ref(false)
const fps = ref(0)
function simulate(x, y) {
viewAngle.x = clamp(x, -1, 1)
viewAngle.y = clamp(y != null ? y : 0, -1, 1)
viewAngle.z = Math.sqrt(viewAngle.x * viewAngle.x + viewAngle.y * viewAngle.y) * 0.5
}
function relax(factor = 0.85) {
viewAngle.x *= factor
viewAngle.y *= factor
viewAngle.z *= factor
}
function onWebGLReady() { isWebGLReady.value = true; hasWebGLError.value = false }
function onWebGLError() { hasWebGLError.value = true; isWebGLReady.value = false }
function onFPSUpdate(v) { fps.value = v }
return {
physics, viewAngle,
isWebGLReady, hasWebGLError, fps,
simulate, relax,
onWebGLReady, onWebGLError, onFPSUpdate,
}
}
export function detectPerformanceTier() {
const mem = navigator.deviceMemory || 4
const cores = navigator.hardwareConcurrency || 4
if (mem <= 2 || cores <= 2) return 'low'
if (mem <= 4 || cores <= 4) return 'mid'
return 'high'
}
export const HOLO_PERFORMANCE_PRESETS = {
high: {
effectIntensity: 0.85, dispersionStrength: 1.0, diffractionScale: 0.7,
highlightSpeed: 0.8, highlightWidth: 1.0, fresnelPower: 3.5,
noiseScale: 1.0, noiseOctaves: 6,
safeZoneRadius: 0.35, safeZoneSoftness: 0.15,
},
mid: {
effectIntensity: 0.75, dispersionStrength: 0.8, diffractionScale: 0.55,
highlightSpeed: 0.7, highlightWidth: 1.1, fresnelPower: 3.0,
noiseScale: 0.8, noiseOctaves: 4,
safeZoneRadius: 0.35, safeZoneSoftness: 0.15,
},
low: {
effectIntensity: 0.6, dispersionStrength: 0.5, diffractionScale: 0.35,
highlightSpeed: 0.5, highlightWidth: 1.3, fresnelPower: 2.5,
noiseScale: 0.55, noiseOctaves: 3,
safeZoneRadius: 0.38, safeZoneSoftness: 0.18,
},
}

View File

@ -189,7 +189,7 @@ export function useLenticularCraftTiltPreview(layersRef) {
/**
* 触摸入口-1..+1 归一化转度数系数 45
* LenticularCard.vue / HolographicCard.vue @simulate 事件触发
* LenticularCard.vue @simulate 事件触发
*/
function simulateTiltFromNormalized(x, y) {
const dx = x * NORMALIZED_TO_DEG

View File

@ -1,557 +0,0 @@
/**
* uni-app App 端可用OSS Post/Put + 签名 GET URL直连抠图默认 IVPD SegmentImage另有 imageseg SegmentHDBodyPOP不依赖 ali-oss / Node http
* 签名使用 Web CryptoHMAC-SHA1避免额外依赖 crypto-js需安全上下文localhost / HTTPS且存在 globalThis.crypto.subtle
*/
const _textEnc = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null
function utf8StringToBase64(str) {
if (_textEnc) {
const bytes = _textEnc.encode(str)
let bin = ''
for (let i = 0; i < bytes.byteLength; i++) {
bin += String.fromCharCode(bytes[i])
}
return btoa(bin)
}
return btoa(unescape(encodeURIComponent(str)))
}
async function hmacSha1Base64(message, secretKey) {
const subtle = globalThis.crypto && globalThis.crypto.subtle
if (!subtle || !_textEnc) {
throw new Error(
'当前环境无法计算 OSS/阿里云签名:需要支持 Web Crypto请在 localhost 或 HTTPS 下打开,并确保非禁用 crypto.subtle 的 WebView'
)
}
const key = await subtle.importKey(
'raw',
_textEnc.encode(secretKey),
{ name: 'HMAC', hash: 'SHA-1' },
false,
['sign']
)
const sig = await subtle.sign('HMAC', key, _textEnc.encode(message))
const bytes = new Uint8Array(sig)
let bin = ''
for (let i = 0; i < bytes.byteLength; i++) {
bin += String.fromCharCode(bytes[i])
}
return btoa(bin)
}
/** RAM 密钥常见复制问题首尾空格、BOM、零宽字符、Windows 多余 \\r */
function normalizeAccessKeyPart(s) {
return String(s || '')
.trim()
.replace(/^\uFEFF/, '')
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '')
.trim()
}
function percentEncode(str) {
if (str === undefined || str === null) {
return ''
}
return encodeURIComponent(String(str))
.replace(/!/g, '%21')
.replace(/'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A')
}
function pickHttpImageUrl(obj, depth = 0) {
if (!obj || depth > 10) return null
if (typeof obj === 'string' && /^https?:\/\//i.test(obj) && /\.(png|jpg|jpeg|webp)/i.test(obj)) {
return obj
}
if (typeof obj !== 'object') return null
for (const v of Object.values(obj)) {
const u = pickHttpImageUrl(v, depth + 1)
if (u) return u
}
return null
}
/** 递归查找 oss://bucket/object 形式 URI */
function pickOssUriString(obj, depth = 0) {
if (!obj || depth > 10) return null
if (typeof obj === 'string' && /^oss:\/\//i.test(obj)) {
if (parseOssUri(obj)) {
return obj
}
}
if (typeof obj !== 'object') return null
for (const v of Object.values(obj)) {
const u = pickOssUriString(v, depth + 1)
if (u) return u
}
return null
}
function parseOssUri(uri) {
const s = String(uri || '').trim()
const m = /^oss:\/\/([^/]+)\/(.+)$/i.exec(s)
if (!m) return null
return { bucket: m[1], objectKey: m[2] }
}
/** PostObject 的目标地址(仅桶根、无 object 路径);失败 errMsg 常只有这一串 URL */
export function looksLikeBareOssEndpointUrl(s) {
const t = String(s || '').trim()
if (!/^https?:\/\/[^/\s]+\.oss-[a-z0-9-]+\.aliyuncs\.com\/?$/i.test(t)) {
return false
}
try {
const u = new URL(t)
if (!/^https?:$/i.test(u.protocol)) return false
const segs = u.pathname.split('/').filter(Boolean)
return segs.length === 0
} catch {
return true
}
}
/** 仅一行 OSS 根域名错误时,转成可操作的说明(依赖 manifest domainWhiteList */
export function humanizeIfBareOssUploadErr(msg) {
const t = String(msg || '').trim()
if (!looksLikeBareOssEndpointUrl(t)) {
return String(msg || '')
}
const hostOnly = t.replace(/^https?:\/\//i, '').replace(/\/$/, '')
return (
`OSS 上传失败uni-app 常因未配置「域名白名单」拦截 uploadFile控制台只显示请求 URL\n\n请在 manifest.json → app-plus → networkSecurity → domainWhiteList 中加入(注意 https\n• https://${hostOnly}\n• IVPDhttps://ivpd.<地域>.aliyuncs.com与 IMM_REGION 一致,如 cn-shanghai\n以及抠图结果下载域名(若与上面不同)。\n保存后必须重新制作自定义调试基座再运行。\n\n原文:${t}`
)
}
/** 排除仅桶根域名、无对象路径的「伪 URL」IMM 偶发只回 Host */
function isLikelyObjectHttpUrl(url) {
try {
const u = new URL(String(url).trim())
if (!/^https?:$/i.test(u.protocol)) {
return false
}
const parts = u.pathname.split('/').filter(Boolean)
return parts.length >= 1
} catch {
return false
}
}
function ossHost(bucket, ossRegionId) {
const r = ossRegionId.replace(/^oss-/, '')
return `${bucket}.oss-${r}.aliyuncs.com`
}
/** objectKey 含路径时用分段编码路径 */
function ossObjectPath(objectKey) {
return objectKey
.split('/')
.map((seg) => encodeURIComponent(seg))
.join('/')
}
/** 私有读:签名 GET URL与 Put 后给分割接口用的 URL 一致) */
export async function ossPresignedObjectUrl({
bucket,
ossRegionId,
accessKeyId,
accessKeySecret,
securityToken,
objectKey,
expiresSec = 3600
}) {
const ak = normalizeAccessKeyPart(accessKeyId)
const sk = normalizeAccessKeyPart(accessKeySecret)
const host = ossHost(bucket, ossRegionId)
const path = ossObjectPath(objectKey)
const resource = `/${bucket}/${objectKey}`
const expires = Math.floor(Date.now() / 1000) + expiresSec
const stringToSignGet = `GET\n\n\n${expires}\n${resource}`
const sigGet = await hmacSha1Base64(stringToSignGet, sk)
let qs = `OSSAccessKeyId=${percentEncode(ak)}&Expires=${expires}&Signature=${percentEncode(sigGet)}`
if (securityToken) {
qs += `&security-token=${percentEncode(securityToken)}`
}
return `https://${host}/${path}?${qs}`
}
/**
* PostObject + policy uni.uploadFile 直传本地路径不经 JS ArrayBuffer调试基座读图失败时的主路径
*/
export async function ossPostObjectFromLocalFilePath({
bucket,
ossRegionId,
accessKeyId,
accessKeySecret,
securityToken,
objectKey,
filePath
}) {
const ak = normalizeAccessKeyPart(accessKeyId)
const sk = normalizeAccessKeyPart(accessKeySecret)
if (!ak || !sk) {
return Promise.reject(new Error('OSS 缺少 AccessKeyId 或 AccessKeySecret'))
}
const expiration = new Date(Date.now() + 55 * 60 * 1000).toISOString()
const conditions = [
['content-length-range', 0, 52428800],
{ bucket },
['eq', '$key', objectKey]
]
if (securityToken) {
conditions.push({ 'x-oss-security-token': securityToken })
}
const policyJson = JSON.stringify({ expiration, conditions })
const policyB64 = utf8StringToBase64(policyJson)
const signature = await hmacSha1Base64(policyB64, sk)
const host = ossHost(bucket, ossRegionId)
const url = `https://${host}/`
const formData = {
key: objectKey,
policy: policyB64,
OSSAccessKeyId: ak,
Signature: signature,
success_action_status: '200'
}
if (securityToken) {
formData['x-oss-security-token'] = securityToken
}
return new Promise((resolve, reject) => {
uni.uploadFile({
url,
filePath,
name: 'file',
formData,
timeout: 180000,
success: (res) => {
const sc = res.statusCode
if (sc >= 200 && sc < 300) {
resolve()
return
}
const body = typeof res.data === 'string' ? res.data : JSON.stringify(res.data || '')
if (typeof body === 'string' && body.includes('SignatureDoesNotMatch')) {
reject(
new Error(
'OSS SignatureDoesNotMatchAccessKeySecret 与当前 OSSAccessKeyId 不匹配,或密钥含隐形字符/换行。请到 RAM 用户页「创建 AccessKey」重新复制 Secret 写入 config/segmentApi.js若曾泄露请删除旧密钥并轮换。'
)
)
return
}
reject(new Error(body || `OSS PostObject 失败 HTTP ${sc}`))
},
fail: (e) => {
const raw = e.errMsg || 'uploadFile 失败'
if (
looksLikeBareOssEndpointUrl(raw) ||
(/request:fail/i.test(raw) && /\.oss-[a-z0-9-]+\.aliyuncs\.com/i.test(raw))
) {
reject(
new Error(
humanizeIfBareOssUploadErr(raw)
)
)
return
}
reject(new Error(raw))
}
})
})
}
function uniRequestRaw(options) {
const timeout = options.timeout != null ? options.timeout : 120000
return new Promise((resolve, reject) => {
uni.request({
...options,
timeout,
success: (r) => {
if (r.statusCode >= 200 && r.statusCode < 300) {
resolve(r)
} else {
const msg =
(typeof r.data === 'string' ? r.data : '') ||
(r.data && r.data.Message) ||
`HTTP ${r.statusCode}`
reject(new Error(msg))
}
},
fail: (e) => reject(new Error(e.errMsg || '请求失败'))
})
})
}
/**
* PUT 上传后生成 OSS 签名 GET URL私有 Bucket 时分割接口需可访问该 URL
*/
export async function ossPutAndSignedGetUrl({
bucket,
ossRegionId,
accessKeyId,
accessKeySecret,
securityToken,
objectKey,
bodyBuffer,
contentType = 'image/jpeg'
}) {
const ak = normalizeAccessKeyPart(accessKeyId)
const sk = normalizeAccessKeyPart(accessKeySecret)
if (!ak || !sk) {
throw new Error('OSS 签名缺少 AccessKeyId 或 AccessKeySecret')
}
const host = ossHost(bucket, ossRegionId)
const path = ossObjectPath(objectKey)
const urlPut = `https://${host}/${path}`
const dateGmt = new Date().toUTCString()
/**
* uni.request 会带 Date若再发 x-oss-dateOSS StringToSign Date + 规范头里的 x-oss-date与错误 XML 一致不能签成 Date 为空
*/
const ossHeaderPairs = []
ossHeaderPairs.push(['x-oss-date', dateGmt])
if (securityToken) {
ossHeaderPairs.push(['x-oss-security-token', securityToken])
}
ossHeaderPairs.sort((a, b) => a[0].localeCompare(b[0]))
const canonHeaders = ossHeaderPairs.map(([k, v]) => `${k}:${v}\n`).join('')
const resource = `/${bucket}/${objectKey}`
const stringToSignPut = `PUT\n\n${contentType}\n${dateGmt}\n${canonHeaders}${resource}`
const sigPut = await hmacSha1Base64(stringToSignPut, sk)
const headersPut = {
Date: dateGmt,
'Content-Type': contentType,
'x-oss-date': dateGmt,
Authorization: `OSS ${ak}:${sigPut}`
}
if (securityToken) {
headersPut['x-oss-security-token'] = securityToken
}
await uniRequestRaw({
url: urlPut,
method: 'PUT',
header: headersPut,
data: bodyBuffer,
/** 大图上传Bitmap 兜底常为 PNG易慢超时后 fail 才会结束 loading */
timeout: 180000
})
return await ossPresignedObjectUrl({
bucket,
ossRegionId,
accessKeyId: ak,
accessKeySecret: sk,
securityToken,
objectKey,
expiresSec: 3600
})
}
/** IVPD 常见英文报错 → 中文说明(控制台需开通「智能视觉生产」等产品) */
function mapIvpdUserMessage(raw) {
const s = String(raw || '')
if (/please\s+open\s+service/i.test(s)) {
return (
'IVPD智能视觉生产服务尚未开通请用阿里云主账号或有权限的子账号登录控制台搜索「智能视觉生产」或「视觉智能开放平台」进入产品页点击「立即开通」后再试抠图。' +
(s.length <= 60 ? `(接口原文:${s}` : '')
)
}
return s
}
/**
* 智能视觉生产 IVPD通用分割 / 抠图SegmentImage
* Endpointivpd.{region}.aliyuncs.comVersion2019-06-25参数 Url 为可访问的图片 HTTP(S) URL OSS 签名 GET
*/
export async function rpcIvpdSegmentImage({
imageUrl,
accessKeyId,
accessKeySecret,
securityToken,
regionId
}) {
const ak = normalizeAccessKeyPart(accessKeyId)
const sk = normalizeAccessKeyPart(accessKeySecret)
const u = String(imageUrl || '').trim()
if (!/^https?:\/\//i.test(u)) {
throw new Error('IVPD SegmentImage 需要可访问的 HTTP(S) 图片 URL一般为 OSS 私有桶签名 GET')
}
const params = {
Format: 'JSON',
Version: '2019-06-25',
AccessKeyId: ak,
SignatureMethod: 'HMAC-SHA1',
Timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
SignatureVersion: '1.0',
SignatureNonce: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
Action: 'SegmentImage',
RegionId: regionId,
Url: u
}
if (securityToken) {
params.SecurityToken = securityToken
}
const sortedKeys = Object.keys(params).sort()
const canonicalized = sortedKeys.map((k) => `${percentEncode(k)}=${percentEncode(params[k])}`).join('&')
const stringToSign = `POST&${percentEncode('/')}&${percentEncode(canonicalized)}`
const signature = await hmacSha1Base64(stringToSign, `${sk}&`)
const body = `${canonicalized}&Signature=${percentEncode(signature)}`
const host = `ivpd.${regionId}.aliyuncs.com`
let res
try {
res = await uniRequestRaw({
url: `https://${host}/`,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: body,
timeout: 120000
})
} catch (e) {
const em = e && e.message ? String(e.message) : ''
if (/please\s+open\s+service/i.test(em)) {
throw new Error(mapIvpdUserMessage(em))
}
try {
const j = JSON.parse(em)
const mm = j.Message || j.message || ''
if (/please\s+open\s+service/i.test(String(mm))) {
throw new Error(mapIvpdUserMessage(mm))
}
} catch (inner) {
if (inner && inner.message && /IVPD/.test(inner.message)) {
throw inner
}
}
throw e
}
let data = res.data
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (e) {
throw new Error((data && data.slice(0, 240)) || 'IVPD 返回非 JSON')
}
}
const flat = data
const bizCode = flat.code ?? flat.Code
const bizMsg = flat.message ?? flat.Message
if (bizCode != null && String(bizCode) !== '0') {
throw new Error(mapIvpdUserMessage(bizMsg ? `${bizCode}: ${bizMsg}` : String(bizCode)))
}
const aliPopMsg = flat.Message || flat.Code
if (typeof aliPopMsg === 'string' && /please\s+open\s+service/i.test(aliPopMsg)) {
throw new Error(mapIvpdUserMessage(aliPopMsg))
}
const result = flat.result ?? flat.Result
const outUrl =
(result && (result.url || result.URL)) ||
flat.url ||
flat.URL ||
pickHttpImageUrl(flat)
if (outUrl && isLikelyObjectHttpUrl(outUrl)) {
return outUrl
}
throw new Error(
bizMsg
? mapIvpdUserMessage(bizMsg)
: `IVPD 未返回有效结果图 URL。摘录${JSON.stringify(flat).slice(0, 600)}`
)
}
/** 人像高清抠图imageseg SegmentHDBody自建代理 server/index.js 等可使用。 */
export async function rpcSegmentHDBody({
imageURL,
accessKeyId,
accessKeySecret,
securityToken,
regionId
}) {
const ak = normalizeAccessKeyPart(accessKeyId)
const sk = normalizeAccessKeyPart(accessKeySecret)
const params = {
Format: 'JSON',
Version: '2019-12-30',
AccessKeyId: ak,
SignatureMethod: 'HMAC-SHA1',
Timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
SignatureVersion: '1.0',
SignatureNonce: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
Action: 'SegmentHDBody',
ImageURL: imageURL
}
if (securityToken) {
params.SecurityToken = securityToken
}
const sortedKeys = Object.keys(params).sort()
const canonicalized = sortedKeys.map((k) => `${percentEncode(k)}=${percentEncode(params[k])}`).join('&')
const stringToSign = `POST&${percentEncode('/')}&${percentEncode(canonicalized)}`
const signature = await hmacSha1Base64(stringToSign, `${sk}&`)
const body = `${canonicalized}&Signature=${percentEncode(signature)}`
const host = `imageseg.${regionId}.aliyuncs.com`
const res = await uniRequestRaw({
url: `https://${host}/`,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: body,
timeout: 90000
})
let data = res.data
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (e) {
throw new Error((data && data.slice(0, 240)) || '分割接口返回非 JSON')
}
}
let flat = data
if (flat && typeof flat.Data === 'string') {
try {
const inner = JSON.parse(flat.Data)
flat = { ...flat, ...inner }
} catch (_) {
/* ignore */
}
}
const outUrl =
flat?.ImageURL ||
flat?.imageURL ||
flat?.Data?.ImageURL ||
pickHttpImageUrl(flat)
if (!outUrl) {
const msg = (flat && (flat.Message || flat.Code)) || JSON.stringify(flat).slice(0, 200)
throw new Error(msg || '分割接口未返回结果图 URL')
}
return outUrl
}

View File

@ -1,135 +0,0 @@
/**
* 镭射卡加权抽卡 5 张不重复,稀有度档位加权 + 5 张保底
*
* 算法说明:
* 1. 单张稀有度按 RARITY_WEIGHTS (50/30/15/4/1) 独立采样
* 2. 同档位内按 style.weight 二次加权选 style
* 3. 5 张不重复(总池 45,远大于 5,无需去重惩罚)
* 4. 5 张保底:若前 4 张全是 N,强制抽 R+(R/SR/SSR/UR 等概率)
* 5. JS,无第三方依赖
*
* 使用:
* - drawGachaStyles(5) 5 style 完整对象( grating_config + bg_prompt )
* - drawGachaStyleIds(5) 5 style ID
*
* 边界:
* - count <= 0 返回空数组
* - count > 池子大小 全部返回(顺序随机)
* - Math.random NaN/Inf 风险
*/
import { LASER_STYLE_POOL } from './stylePool.js'
/** @typedef {('N'|'R'|'SR'|'SSR'|'UR')} Rarity */
/** 档位权重(总和 100,代表单次抽卡的稀有度分布百分比) */
export const RARITY_WEIGHTS = /** @type {Record<Rarity, number>} */ ({
N: 50,
R: 30,
SR: 15,
SSR: 4,
UR: 1,
})
/** 保底用:抽 R+ 时各档位等概率 */
const R_PLUS_BUCKETS = /** @type {Rarity[]} */ (['R', 'SR', 'SSR', 'UR'])
/**
* 加权随机选一个稀有度档位
* @returns {Rarity}
*/
export function pickRarity() {
const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0)
let r = Math.random() * total
for (const [rarity, w] of Object.entries(RARITY_WEIGHTS)) {
r -= /** @type {number} */ (w)
if (r <= 0) return /** @type {Rarity} */ (rarity)
}
return 'N'
}
/**
* 在指定档位内按 style.weight 加权抽一个,且不与 exclude 重复
* @param {Rarity} rarity
* @param {Set<string>} exclude 已选 style id 集合
* @returns {import('./stylePool.js').LaserStyle | null}
*/
export function pickStyleInRarity(rarity, exclude) {
const candidates = LASER_STYLE_POOL.filter(
(s) => s.rarity === rarity && !exclude.has(s.id),
)
if (candidates.length === 0) return null
const sum = candidates.reduce((a, s) => a + (s.weight || 0), 0) || 1
let r = Math.random() * sum
for (const s of candidates) {
r -= s.weight || 0
if (r <= 0) return s
}
return candidates[candidates.length - 1]
}
/**
* 加权抽卡:5 张不重复,保底 1 R+
* @param {number} [count=5] 抽卡数量
* @returns {Array<import('./stylePool.js').LaserStyle>}
*/
export function drawGachaStyles(count = 5) {
const n = Math.max(0, Math.floor(count))
if (n === 0) return []
if (n >= LASER_STYLE_POOL.length) {
// 数量超过池子大小 → 全部返回(洗牌)
const all = LASER_STYLE_POOL.slice()
for (let i = all.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[all[i], all[j]] = [all[j], all[i]]
}
return all
}
const picked = /** @type {Array<import('./stylePool.js').LaserStyle>} */ ([])
const used = new Set()
for (let i = 0; i < n; i++) {
let rarity = pickRarity()
// 第 n 张(最后一张)保底:前 n-1 张全是 N → 强制 R+
if (i === n - 1 && picked.length > 0 && picked.every((s) => s.rarity === 'N')) {
rarity = R_PLUS_BUCKETS[Math.floor(Math.random() * R_PLUS_BUCKETS.length)]
}
let style = pickStyleInRarity(rarity, used)
// 同档位被抽空 → 降级到任意可用档位
if (!style) {
style = pickStyleInRarity('N', used)
if (!style) {
// 极端兜底:任意未选 style
style = LASER_STYLE_POOL.find((s) => !used.has(s.id)) || null
}
}
if (!style) break
picked.push(style)
used.add(style.id)
}
return picked
}
/**
* 便捷函数:只返回 ID 列表
* @param {number} [count=5]
* @returns {string[]}
*/
export function drawGachaStyleIds(count = 5) {
return drawGachaStyles(count).map((s) => s.id)
}
/**
* 工具: preset_id 反查 style(供调试 / 日志使用)
* @param {string} id
* @returns {import('./stylePool.js').LaserStyle | undefined}
*/
export function findStyleById(id) {
return LASER_STYLE_POOL.find((s) => s.id === id)
}

View File

@ -141,55 +141,3 @@ export function buildRenderConfigs(presetIds) {
}
export { resolveGratingConfig, DEFAULT_BACKDROP_TONE } from './laserGrating.js'
// =========================================================================
// 新增:风格池 + 抽卡(45 个风格,5 档分层)
// - 旧调用方(激光导出会、success.vue)继续用上方 export,完全兼容
// - 新调用方(抽卡、稀有度徽章)使用下方 export
// =========================================================================
// 风格池与索引(转自 stylePool.js)
export {
LASER_STYLE_POOL,
LASER_STYLE_IDS,
LASER_STYLE_INDEX,
RARITY_DISTRIBUTION,
RARITY_COLORS,
RARITY_STARS,
} from './stylePool.js'
// 抽卡算法(转自 gacha.js)
export {
drawGachaStyles,
drawGachaStyleIds,
pickRarity,
pickStyleInRarity,
findStyleById,
RARITY_WEIGHTS,
} from './gacha.js'
/**
* 兼容旧调用方:按数字索引取 style 对象
* 0-4 仍映射到原 dream/classic/holoFull/ice/sunset(与旧 PRESET_VARIANTS 顺序一致)
* 5~44 映射到新池子
* @param {number} index
*/
export function getStyleByIndex(index) {
const i = Number(index)
if (!Number.isFinite(i) || i < 0) return LASER_STYLE_POOL[0]
if (i < 5) {
const oldIds = ['dream', 'classic', 'holoFull', 'ice', 'sunset']
return LASER_STYLE_POOL.find((s) => s.id === oldIds[i]) || LASER_STYLE_POOL[0]
}
return LASER_STYLE_POOL[i] || LASER_STYLE_POOL[0]
}
/**
* 兼容旧调用方: style id 反查数字索引(用于 success.vue)
* @param {string} id
* @returns {number}
*/
export function getStyleIndexById(id) {
const idx = LASER_STYLE_INDEX[String(id || '')]
return Number.isFinite(idx) ? idx : 0
}

View File

@ -1,44 +0,0 @@
/**
* 智能抠图生产环境走网关 POST /api/v1/segment useLaserSegment.js
* 下列 direct 配置仅作本地调试遗留正式包勿填 AK
*/
/** 仅开发RAM 子账号 AccessKeyId勿提交仓库、勿用于正式包 */
export const ALIYUN_ACCESS_KEY_ID = ''
/** 仅开发RAM 子账号 AccessKeySecret */
export const ALIYUN_ACCESS_KEY_SECRET = ''
/** 可选:使用 STS 临时凭证时填写 SecurityToken长期 AK 一般留空 */
export const ALIYUN_SECURITY_TOKEN = ''
/** 生产STS 获取地址GET返回 JSON。若上面已填 AK/SK则不再请求此地址 */
export const ALIYUN_STS_URL = ''
/** OSS Bucket 名(不含域名) */
export const OSS_BUCKET = ''
/** OSS region如 oss-cn-shanghai */
export const OSS_REGION = 'oss-cn-shanghai'
/** 上传目录前缀 */
export const OSS_KEY_PREFIX = 'laser-card-segment/tmp/'
/** imageseg 地域,如 cn-shanghai */
export const IMAGESEG_REGION = 'cn-shanghai'
/** 预留;当前直连抠图走 IVPD不使用 IMM 项目名称 */
export const IMM_PROJECT_NAME = 'laser-card'
/** IVPD 与 OSS 同地域 id如 cn-shanghai */
export const IMM_REGION = 'cn-shanghai'
/**
* auto | backend | direct 详见 segmentApi.example.js 注释接后端时改为 backend 并填 SEGMENT_API_BASE
*/
export const SEGMENT_TRANSPORT = 'backend'
/** 方案二:自建代理根地址(无末尾斜杠);与方案一互斥:有 OSS 且AK 或 STS时不会走代理 */
export const SEGMENT_API_BASE = ''
export const SEGMENT_API_TOKEN = ''

View File

@ -1,320 +0,0 @@
import {
ALIYUN_ACCESS_KEY_ID,
ALIYUN_ACCESS_KEY_SECRET,
ALIYUN_SECURITY_TOKEN,
ALIYUN_STS_URL,
IMM_REGION,
OSS_BUCKET,
OSS_KEY_PREFIX,
OSS_REGION,
SEGMENT_API_BASE,
SEGMENT_API_TOKEN,
SEGMENT_TRANSPORT
} from './segmentApi.js'
import {
ossPostObjectFromLocalFilePath,
ossPresignedObjectUrl,
rpcIvpdSegmentImage,
looksLikeBareOssEndpointUrl
} from './aliyunPortraitUni.js'
function uniRequest(options) {
return new Promise((resolve, reject) => {
uni.request({
...options,
success: (r) => {
if (r.statusCode >= 200 && r.statusCode < 300) {
resolve(r)
} else {
const msg =
(typeof r.data === 'object' && r.data && (r.data.Message || r.data.message)) ||
(typeof r.data === 'string' ? r.data : '') ||
`HTTP ${r.statusCode}`
reject(new Error(msg))
}
},
fail: (e) => reject(new Error(e.errMsg || '网络请求失败'))
})
})
}
/** 内层 Promise 长期无回调时强制失败(避免一直「正在识别」) */
function raceWithTimeout(promise, ms, errMsg) {
return new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error(errMsg)), ms)
Promise.resolve(promise).then(
(v) => {
clearTimeout(t)
resolve(v)
},
(e) => {
clearTimeout(t)
reject(e)
}
)
})
}
function normalizeStsPayload(raw) {
if (!raw || typeof raw !== 'object') {
return null
}
if (raw.Credentials) {
const c = raw.Credentials
return {
accessKeyId: c.AccessKeyId,
accessKeySecret: c.AccessKeySecret,
securityToken: c.SecurityToken || undefined,
expiration: c.Expiration
}
}
if (raw.accessKeyId && raw.accessKeySecret) {
return {
accessKeyId: raw.accessKeyId,
accessKeySecret: raw.accessKeySecret,
securityToken: raw.securityToken || undefined,
expiration: raw.expiration
}
}
return null
}
async function fetchStsCredentials() {
const url = (ALIYUN_STS_URL || '').trim()
if (!url) {
return null
}
const res = await uniRequest({ url, method: 'GET', timeout: 30000 })
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
const cred = normalizeStsPayload(data)
if (!cred || !cred.accessKeyId) {
throw new Error('STS 地址返回格式不正确(需 Credentials 或 accessKeyId/accessKeySecret')
}
return cred
}
/** 开发直配 AK或 STS 拉取 */
function getCredentialsFromConfig() {
const id = (ALIYUN_ACCESS_KEY_ID || '').trim()
const sec = (ALIYUN_ACCESS_KEY_SECRET || '').trim()
const token = (ALIYUN_SECURITY_TOKEN || '').trim()
if (id && sec) {
return {
accessKeyId: id,
accessKeySecret: sec,
securityToken: token || undefined
}
}
return null
}
async function resolveCredentials() {
const fromFile = getCredentialsFromConfig()
if (fromFile) {
return fromFile
}
const url = (ALIYUN_STS_URL || '').trim()
if (!url) {
throw new Error('请配置 ALIYUN_ACCESS_KEY_ID/SECRET开发或 ALIYUN_STS_URL生产')
}
return fetchStsCredentials()
}
function canUseDirectAliyunClient() {
const bucket = (OSS_BUCKET || '').trim()
const hasAk =
!!(ALIYUN_ACCESS_KEY_ID || '').trim() && !!(ALIYUN_ACCESS_KEY_SECRET || '').trim()
const hasStsUrl = !!(ALIYUN_STS_URL || '').trim()
return !!(bucket && (hasAk || hasStsUrl))
}
/**
* @returns {'direct' | 'backend'}
*/
function resolveSegmentTransport() {
const raw = String(SEGMENT_TRANSPORT == null ? 'auto' : SEGMENT_TRANSPORT)
.trim()
.toLowerCase()
if (raw === 'backend' || raw === 'proxy' || raw === 'server') {
return 'backend'
}
if (raw === 'direct' || raw === 'aliyun' || raw === 'client' || raw === 'oss') {
if (!canUseDirectAliyunClient()) {
throw new Error(
'SEGMENT_TRANSPORT 为 direct/aliyun/client/oss 时需配置 OSS_BUCKET 以及ALIYUN_ACCESS_KEY_ID+SECRET 或 ALIYUN_STS_URL'
)
}
return 'direct'
}
return canUseDirectAliyunClient() ? 'direct' : 'backend'
}
/**
* 凭证 OSS PostObject 签名 GET URL IVPD SegmentImageUrl downloadFile
*/
async function segmentViaOssIvpd(filePath) {
const bucket = (OSS_BUCKET || '').trim()
if (!bucket) {
throw new Error('请在 config/segmentApi.js 配置 OSS_BUCKET')
}
const cred = await resolveCredentials()
const ext = /\.png$/i.test(String(filePath || '')) ? 'png' : 'jpg'
const base = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
const prefix = OSS_KEY_PREFIX.replace(/^\/+/, '')
const objectKeyIn = `${prefix}${base}.${ext}`
await ossPostObjectFromLocalFilePath({
bucket,
ossRegionId: OSS_REGION,
accessKeyId: cred.accessKeyId,
accessKeySecret: cred.accessKeySecret,
securityToken: cred.securityToken,
objectKey: objectKeyIn,
filePath
})
const imageUrlForSeg = await ossPresignedObjectUrl({
bucket,
ossRegionId: OSS_REGION,
accessKeyId: cred.accessKeyId,
accessKeySecret: cred.accessKeySecret,
securityToken: cred.securityToken,
objectKey: objectKeyIn,
expiresSec: 3600
})
const ivpdRegion = (IMM_REGION || '').trim() || String(OSS_REGION || '').replace(/^oss-/i, '')
const outUrl = await rpcIvpdSegmentImage({
imageUrl: imageUrlForSeg,
accessKeyId: cred.accessKeyId,
accessKeySecret: cred.accessKeySecret,
securityToken: cred.securityToken,
regionId: ivpdRegion
})
return raceWithTimeout(
new Promise((res, rej) => {
uni.downloadFile({
url: outUrl,
timeout: 120000,
success: (dr) => {
if (dr.statusCode !== 200 || !dr.tempFilePath) {
rej(new Error('下载分割结果失败'))
return
}
res({ kind: 'cutout', localPath: dr.tempFilePath })
},
fail: (e) => {
const raw = e.errMsg || '未知错误'
const needDomainHint =
looksLikeBareOssEndpointUrl(raw) ||
(/^https?:\/\//i.test(String(raw).trim()) && String(raw).includes('aliyuncs'))
const extra = needDomainHint
? '(若仅为 URL请在 manifest → networkSecurity.domainWhiteList 中加入下载域名并重编译。)'
: ''
rej(new Error(`下载分割结果失败:${raw}${extra}`))
}
})
}),
125000,
'下载分割结果超时'
)
}
/**
* 方案二自建 Node 代理server/index.js
*/
function segmentViaProxy(filePath) {
const base = (SEGMENT_API_BASE || '').trim().replace(/\/+$/, '')
if (!base) {
return Promise.reject(
new Error(
'缺少 SEGMENT_API_BASEHTTP/后端抠图需要填写根地址POST {BASE}/segment。若使用客户端直连阿里云请配置 OSS_BUCKET 与 AK 或 STS并将 SEGMENT_TRANSPORT 设为 auto 或 direct。'
)
)
}
const url = `${base}/segment`
return new Promise((resolve, reject) => {
uni.uploadFile({
url,
filePath,
name: 'image',
timeout: 120000,
header: SEGMENT_API_TOKEN ? { 'x-laser-token': SEGMENT_API_TOKEN } : {},
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error(typeof res.data === 'string' ? res.data : `HTTP ${res.statusCode}`))
return
}
let body
try {
body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
} catch (e) {
reject(new Error('代理返回非 JSON'))
return
}
if (!body || body.ok !== true || !body.imageUrl) {
reject(new Error((body && body.error) || '抠图失败'))
return
}
uni.downloadFile({
url: body.imageUrl,
timeout: 120000,
success: (dr) => {
if (dr.statusCode !== 200 || !dr.tempFilePath) {
reject(new Error('下载分割结果失败'))
return
}
resolve({ kind: 'cutout', localPath: dr.tempFilePath })
},
fail: (e) => reject(new Error(e.errMsg || 'downloadFile 失败'))
})
},
fail: (e) => reject(new Error(e.errMsg || 'uploadFile 失败'))
})
})
}
/** 防止某一步网络/原生无回调导致一直转圈 */
function withTimeout(promise, ms, message) {
let timer = null
return new Promise((resolve, reject) => {
timer = setTimeout(() => {
timer = null
reject(new Error(message))
}, ms)
promise.then(
(v) => {
if (timer != null) {
clearTimeout(timer)
}
resolve(v)
},
(e) => {
if (timer != null) {
clearTimeout(timer)
}
reject(e)
}
)
})
}
/**
* 上传本地图并获取抠图结果本地路径
* 链路由 config/segmentApi.js SEGMENT_TRANSPORT 决定direct=客户端 OSS + IVPD SegmentImagebackend=HTTP 后端契约同 server/index.jsauto=能直连则直连否则走后端
*/
export function segmentPortraitToLocal(filePath) {
const transport = resolveSegmentTransport()
const inner =
transport === 'direct' ? segmentViaOssIvpd(filePath) : segmentViaProxy(filePath)
return withTimeout(
inner,
240000,
'智能抠图超时请检查手机网络、OSS、IVPD 地域IMM_REGION如 cn-shanghai、RAM 权限及 manifest 网络权限'
)
}

View File

@ -1,533 +0,0 @@
/**
* 镭射卡风格池 45 个风格,5 档分层(N/R/SR/SSR/UR)
*
* 设计原则:
* 1. 每个 style 自包含 grating_config + bg_prompt + overlay_prompt,可独立喂给 compositor
* 2. 真实参考(reference 字段)指向实体卡牌系列,设计师可回溯优化
* 3. 权重(weight)**同档位内**有效;跨档位权重由 gacha.js RARITY_WEIGHTS 控制
* 4. 全英文 prompt 利于 AI 生图;负向提示"no text, no people, no watermark"防跑偏
* 5. bg_prompt 显式标注 sheen 角度/速度, grating_config 对齐,避免"参数是冷银但生图是紫色"的不一致
*
* 档位分布:
* - N(50%):日常量产,银底 + 单色带
* - R(30%):Refractor / Holo Rare 级常规工艺
* - SR(15%):限量,Mosaic / Atomic / 横纹类
* - SSR(4%):稀有,Full Art / Rainbow / Gold
* - UR(1%):终极,Secret / Star / Prismatic Secret
*
* @typedef {('N'|'R'|'SR'|'SSR'|'UR')} Rarity
* @typedef {Object} LaserStyle
* @property {string} id
* @property {Rarity} rarity
* @property {string} display_name_zh
* @property {string} display_name_en
* @property {number} weight
* @property {string} family
* @property {{series: string, year: string, note: string}} reference
* @property {{sheen_band_angle: number, sheen_speed: number, sheen_intensity: number, foil_coverage: number, backdrop_tone: string}} grating_config
* @property {string} color_hint
* @property {string} bg_prompt
* @property {string} overlay_prompt
* @property {string} tags
*/
export const LASER_STYLE_POOL = [
// ============ N 档(10 个,基础银底工艺)============
{
id: 'classic_silver', rarity: 'N', display_name_zh: '经典银底', display_name_en: 'Classic Silver', weight: 12,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Silver Prizm', year: '2018-', note: '入门款 Prizm, 单角度折射' },
grating_config: { sheen_band_angle: 120, sheen_speed: 0.40, sheen_intensity: 0.32, foil_coverage: 0.55, backdrop_tone: '#A8ACB2' },
color_hint: '冷银 浅灰',
bg_prompt: 'pure silver holographic background, soft brushed metal texture, single subtle diagonal sheen band, neutral gray gradient, clean cardstock finish, minimalist industrial design, no text, no people, no watermark',
overlay_prompt: 'thin silver border, single diagonal light line, small geometric corner ornaments',
tags: 'classic,base,entry',
},
{
id: 'moonlight_silver', rarity: 'N', display_name_zh: '月光银', display_name_en: 'Moonlight Silver', weight: 10,
family: 'pokemon_tcg',
reference: { series: 'Pokémon TCG Base Set Holo', year: '1999', note: '初代宝可梦 holo, 微彩虹折射' },
grating_config: { sheen_band_angle: 115, sheen_speed: 0.35, sheen_intensity: 0.30, foil_coverage: 0.50, backdrop_tone: '#A89E91' },
color_hint: '月光银 香槟银',
bg_prompt: 'gentle moonlight silver holographic gradient, soft brushed metal with micro sparkles, muted silver-to-pearl shift, calm celestial mood, clean pastel undertones, no text, no people, no watermark',
overlay_prompt: 'thin pearl border, soft star scatter, subtle corner brackets',
tags: 'soft,pastel',
},
{
id: 'pearl_basic', rarity: 'N', display_name_zh: '珍珠银', display_name_en: 'Pearl Basic', weight: 10,
family: 'topps_chrome',
reference: { series: 'Topps Chrome Refractor (Base)', year: '1996-', note: 'Topps Chrome 入门反色' },
grating_config: { sheen_band_angle: 130, sheen_speed: 0.38, sheen_intensity: 0.34, foil_coverage: 0.58, backdrop_tone: '#B0ACA6' },
color_hint: '珍珠白 淡奶白',
bg_prompt: 'pearl white brushed metal holographic surface, soft luminous glow, neutral off-white gradient with faint iridescent shimmer, clean minimalist backdrop, no text, no people, no watermark',
overlay_prompt: 'fine pearl-white border line, gentle corner ornaments, no harsh shapes',
tags: 'pearl,clean',
},
{
id: 'frost_silver', rarity: 'N', display_name_zh: '霜银', display_name_en: 'Frost Silver', weight: 10,
family: 'pokemon_tcg',
reference: { series: 'Pokémon TCG Reverse Holo (Basic)', year: '2003-', note: '宝可梦基础系列反 holo' },
grating_config: { sheen_band_angle: 110, sheen_speed: 0.42, sheen_intensity: 0.30, foil_coverage: 0.48, backdrop_tone: '#9C9993' },
color_hint: '冰蓝 浅灰银',
bg_prompt: 'frosted silver metal surface with cool blue undertone, soft frosted glass texture, gentle vertical sheen band, clean icy minimalism, no text, no people, no watermark',
overlay_prompt: 'thin frosted edge line, sparse ice flake scatter, light frost corner motif',
tags: 'cool,frost',
},
{
id: 'warm_silver', rarity: 'N', display_name_zh: '暖银', display_name_en: 'Warm Silver', weight: 10,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Base (Retail)', year: '2020-', note: '零售版基础款' },
grating_config: { sheen_band_angle: 125, sheen_speed: 0.40, sheen_intensity: 0.32, foil_coverage: 0.52, backdrop_tone: '#A89E91' },
color_hint: '暖银 浅米色',
bg_prompt: 'warm silver holographic base, soft champagne undertone, brushed metal texture with subtle warmth, gentle pearl shine, no text, no people, no watermark',
overlay_prompt: 'thin warm silver border, small geometric corners',
tags: 'warm,classic',
},
{
id: 'graphite_silver', rarity: 'N', display_name_zh: '石墨银', display_name_en: 'Graphite Silver', weight: 10,
family: 'panini_prizm',
reference: { series: 'Panini Prizm (Retail Hanger)', year: '2019-', note: '银底素色基础款' },
grating_config: { sheen_band_angle: 135, sheen_speed: 0.36, sheen_intensity: 0.30, foil_coverage: 0.50, backdrop_tone: '#9C9993' },
color_hint: '石墨灰 冷灰银',
bg_prompt: 'graphite silver brushed metal surface, neutral cool gray gradient, single soft diagonal sheen, industrial clean mood, no text, no people, no watermark',
overlay_prompt: 'thin graphite border, sparse geometric lines',
tags: 'industrial,cool',
},
{
id: 'rosegold_base', rarity: 'N', display_name_zh: '玫瑰金底', display_name_en: 'Rose Gold Base', weight: 10,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Rose Gold (Retail)', year: '2020-', note: '零售版玫瑰金反色' },
grating_config: { sheen_band_angle: 140, sheen_speed: 0.40, sheen_intensity: 0.34, foil_coverage: 0.55, backdrop_tone: '#9E958A' },
color_hint: '玫瑰金 浅粉金',
bg_prompt: 'soft rose gold holographic base, warm pinkish metallic gradient, subtle champagne sheen, gentle romantic mood, no text, no people, no watermark',
overlay_prompt: 'thin rose gold border, small heart corner accents',
tags: 'romantic,warm',
},
{
id: 'sky_silver', rarity: 'N', display_name_zh: '天空银', display_name_en: 'Sky Silver', weight: 10,
family: 'topps_chrome',
reference: { series: 'Topps Chrome Sky Refractor', year: '2021-', note: '天空蓝基础款(非 1/1)' },
grating_config: { sheen_band_angle: 105, sheen_speed: 0.44, sheen_intensity: 0.32, foil_coverage: 0.50, backdrop_tone: '#A8ACB2' },
color_hint: '天蓝 银白',
bg_prompt: 'sky blue tinted silver holographic base, soft daylight gradient with cool silver undertones, airy clean minimalism, no text, no people, no watermark',
overlay_prompt: 'thin sky blue border, soft cloud scatter',
tags: 'sky,cool',
},
{
id: 'cream_silver', rarity: 'N', display_name_zh: '奶白银', display_name_en: 'Cream Silver', weight: 9,
family: 'weiss',
reference: { series: 'Weiss Schwarz R (Common)', year: '2008-', note: 'WS 普通卡基础反色' },
grating_config: { sheen_band_angle: 145, sheen_speed: 0.38, sheen_intensity: 0.30, foil_coverage: 0.46, backdrop_tone: '#B0ACA6' },
color_hint: '奶白 米黄',
bg_prompt: 'cream ivory silver base, soft warm white metallic gradient, gentle sheen, clean Japanese-card aesthetic, no text, no people, no watermark',
overlay_prompt: 'thin cream border, sparse sakura scatter',
tags: 'japan,soft',
},
{
id: 'steel_silver', rarity: 'N', display_name_zh: '钢银', display_name_en: 'Steel Silver', weight: 9,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Hyper (Base Tier)', year: '2019-', note: 'Hyper 基础款' },
grating_config: { sheen_band_angle: 150, sheen_speed: 0.34, sheen_intensity: 0.30, foil_coverage: 0.52, backdrop_tone: '#9C9993' },
color_hint: '钢灰 冷银',
bg_prompt: 'cold steel silver holographic base, cool industrial brushed metal, sharp single diagonal sheen, masculine minimal mood, no text, no people, no watermark',
overlay_prompt: 'thin steel border, small angular corner motif',
tags: 'industrial,sharp',
},
// ============ R 档(10 个,Refractor/反 holo 工艺)============
{
id: 'prizm_blue', rarity: 'R', display_name_zh: '蓝棱镜', display_name_en: 'Prizm Blue', weight: 7,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Blue Prizm', year: '2018-', note: '蓝棱镜折射,真卡标志款' },
grating_config: { sheen_band_angle: 120, sheen_speed: 0.42, sheen_intensity: 0.38, foil_coverage: 0.70, backdrop_tone: '#A8ACB2' },
color_hint: '电光蓝 宝石蓝',
bg_prompt: 'electric blue prismatic holographic surface, sapphire and cyan layered sheen, vivid blue-to-silver diagonal band, sport card premium mood, sharp reflective energy, no text, no people, no watermark',
overlay_prompt: 'clean blue bevel border, sharp diagonal sheen sweep, minimal geometric accents',
tags: 'blue,sport,prizm',
},
{
id: 'prizm_red', rarity: 'R', display_name_zh: '红棱镜', display_name_en: 'Prizm Red', weight: 7,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Red Prizm', year: '2018-', note: '红棱镜折射,真卡标志款' },
grating_config: { sheen_band_angle: 125, sheen_speed: 0.40, sheen_intensity: 0.38, foil_coverage: 0.70, backdrop_tone: '#A89E91' },
color_hint: '宝石红 深粉红',
bg_prompt: 'ruby red prismatic holographic surface, crimson and rose layered sheen, vivid red-to-silver diagonal band, premium sport card energy, no text, no people, no watermark',
overlay_prompt: 'clean red bevel border, sharp diagonal sheen sweep, minimal geometric accents',
tags: 'red,sport,prizm',
},
{
id: 'prizm_green', rarity: 'R', display_name_zh: '绿棱镜', display_name_en: 'Prizm Green', weight: 6,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Green Prizm', year: '2018-', note: '绿棱镜折射' },
grating_config: { sheen_band_angle: 130, sheen_speed: 0.40, sheen_intensity: 0.38, foil_coverage: 0.70, backdrop_tone: '#A8ACB2' },
color_hint: '祖母绿 翡翠',
bg_prompt: 'emerald green prismatic holographic surface, forest and mint layered sheen, vivid green-to-silver diagonal band, sport card vitality, no text, no people, no watermark',
overlay_prompt: 'clean green bevel border, sharp diagonal sheen sweep',
tags: 'green,sport,prizm',
},
{
id: 'prizm_orange', rarity: 'R', display_name_zh: '橙棱镜', display_name_en: 'Prizm Orange', weight: 6,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Orange Prizm', year: '2018-', note: '橙棱镜折射' },
grating_config: { sheen_band_angle: 115, sheen_speed: 0.42, sheen_intensity: 0.38, foil_coverage: 0.68, backdrop_tone: '#9E958A' },
color_hint: '橙色 琥珀色',
bg_prompt: 'vivid orange prismatic holographic surface, tangerine and amber layered sheen, sharp orange-to-silver diagonal band, no text, no people, no watermark',
overlay_prompt: 'clean orange bevel border, sharp diagonal sheen sweep',
tags: 'orange,sport,prizm',
},
{
id: 'prizm_purple', rarity: 'R', display_name_zh: '紫棱镜', display_name_en: 'Prizm Purple', weight: 6,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Purple Prizm', year: '2018-', note: '紫棱镜折射' },
grating_config: { sheen_band_angle: 110, sheen_speed: 0.40, sheen_intensity: 0.38, foil_coverage: 0.70, backdrop_tone: '#9C9993' },
color_hint: '紫水晶 葡萄紫',
bg_prompt: 'amethyst purple prismatic holographic surface, violet and magenta layered sheen, vivid purple-to-silver diagonal band, no text, no people, no watermark',
overlay_prompt: 'clean purple bevel border, sharp diagonal sheen sweep',
tags: 'purple,sport,prizm',
},
{
id: 'topps_refractor', rarity: 'R', display_name_zh: '经典折射', display_name_en: 'Topps Refractor', weight: 6,
family: 'topps_chrome',
reference: { series: 'Topps Chrome Refractor', year: '1996-', note: 'Topps Chrome 标志款折射' },
grating_config: { sheen_band_angle: 125, sheen_speed: 0.40, sheen_intensity: 0.36, foil_coverage: 0.68, backdrop_tone: '#A8ACB2' },
color_hint: '彩虹光 全光谱',
bg_prompt: 'classic baseball card refractor holographic surface, full spectrum rainbow sheen sweeping diagonally, sharp multilayer reflective layers, vintage baseball card mood, no text, no people, no watermark',
overlay_prompt: 'clean silver bevel border, single bright diagonal sheen line',
tags: 'refractor,rainbow,sport',
},
{
id: 'reverse_holo', rarity: 'R', display_name_zh: '反面全息', display_name_en: 'Reverse Holo', weight: 5,
family: 'pokemon_tcg',
reference: { series: 'Pokémon TCG Reverse Holo', year: '2003-', note: '宝可梦反面全息(图案外全息)' },
grating_config: { sheen_band_angle: 105, sheen_speed: 0.38, sheen_intensity: 0.34, foil_coverage: 0.62, backdrop_tone: '#A8ACB2' },
color_hint: '浅银 柔彩虹',
bg_prompt: 'reverse holo pattern background, soft rainbow shimmer surrounding a centered art window, gentle pastel shimmer band, clean Japanese TCG aesthetic, no text, no people, no watermark',
overlay_prompt: 'thin silver border around art window, gentle sparkle scatter, modest corner brackets',
tags: 'pokemon,reverse',
},
{
id: 'bowman_chrome', rarity: 'R', display_name_zh: 'Bowman 折射', display_name_en: 'Bowman Chrome', weight: 5,
family: 'topps_chrome',
reference: { series: 'Bowman Chrome Refractor', year: '2008-', note: 'Bowman Chrome 棒球卡折射' },
grating_config: { sheen_band_angle: 135, sheen_speed: 0.40, sheen_intensity: 0.36, foil_coverage: 0.65, backdrop_tone: '#A89E91' },
color_hint: '暖银 浅金',
bg_prompt: 'warm silver Bowman Chrome refractor surface, soft warm metallic gradient with single diagonal sheen, clean baseball prospect card mood, no text, no people, no watermark',
overlay_prompt: 'thin warm silver border, single diagonal sheen line',
tags: 'baseball,sport',
},
{
id: 'prizm_pink', rarity: 'R', display_name_zh: '粉棱镜', display_name_en: 'Prizm Pink', weight: 5,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Pink Prizm (WBC)', year: '2023-', note: '粉棱镜折射' },
grating_config: { sheen_band_angle: 120, sheen_speed: 0.40, sheen_intensity: 0.36, foil_coverage: 0.68, backdrop_tone: '#9E958A' },
color_hint: '樱花粉 玫瑰粉',
bg_prompt: 'cherry blossom pink prismatic holographic surface, soft rose and magenta layered sheen, vivid pink-to-silver diagonal band, no text, no people, no watermark',
overlay_prompt: 'clean pink bevel border, soft floral corner accents',
tags: 'pink,sport,prizm',
},
{
id: 'wave_blue', rarity: 'R', display_name_zh: '波浪蓝', display_name_en: 'Wave Blue', weight: 5,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Wave', year: '2021-', note: 'Wave 波浪折射' },
grating_config: { sheen_band_angle: 100, sheen_speed: 0.46, sheen_intensity: 0.34, foil_coverage: 0.65, backdrop_tone: '#A8ACB2' },
color_hint: '海蓝 浪花白',
bg_prompt: 'wave-patterned blue holographic surface, layered horizontal ocean wave sheen, soft sea blue gradient with silver crests, no text, no people, no watermark',
overlay_prompt: 'clean silver border, soft wave corner motif',
tags: 'wave,blue,sport',
},
// ============ SR 档(10 个,Mosaic/Atomic/Neon 类限量工艺)============
{
id: 'mosaic', rarity: 'SR', display_name_zh: '马赛克', display_name_en: 'Mosaic', weight: 3,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Mosaic', year: '2019-', note: 'Mosaic 马赛克拼贴反色' },
grating_config: { sheen_band_angle: 115, sheen_speed: 0.44, sheen_intensity: 0.40, foil_coverage: 0.78, backdrop_tone: '#A8ACB2' },
color_hint: '多彩 马赛克',
bg_prompt: 'mosaic tiled holographic surface, geometric tile pattern with shimmer variation between cells, prismatic rainbow sheen across tile grid, structured luxury card mood, no text, no people, no watermark',
overlay_prompt: 'geometric mosaic border, tile corner motif',
tags: 'mosaic,pattern',
},
{
id: 'shimmer', rarity: 'SR', display_name_zh: '微光闪片', display_name_en: 'Shimmer', weight: 3,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Shimmer', year: '2020-', note: 'Shimmer 闪光颗粒反色' },
grating_config: { sheen_band_angle: 125, sheen_speed: 0.42, sheen_intensity: 0.40, foil_coverage: 0.76, backdrop_tone: '#B0ACA6' },
color_hint: '珍珠光 颗粒银',
bg_prompt: 'shimmer micro-flake holographic surface, dense pearl flake particles with prismatic sparkle, soft radiant metallic gradient, glitter-card premium mood, no text, no people, no watermark',
overlay_prompt: 'fine pearl border, sparkle scatter',
tags: 'shimmer,sparkle',
},
{
id: 'neon_molectric', rarity: 'SR', display_name_zh: '霓虹电光', display_name_en: 'Neon Molectric', weight: 3,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Neon Molectric', year: '2021-', note: '霓虹电光反色' },
grating_config: { sheen_band_angle: 110, sheen_speed: 0.46, sheen_intensity: 0.44, foil_coverage: 0.78, backdrop_tone: '#9C9993' },
color_hint: '霓虹粉 电光绿',
bg_prompt: 'neon molectric holographic surface, vivid neon pink and electric green sheen sweep, dark silver base with bright color sparks, cyber-arcade premium mood, no text, no people, no watermark',
overlay_prompt: 'neon border, sharp diagonal sheen sweep',
tags: 'neon,cyber',
},
{
id: 'disco', rarity: 'SR', display_name_zh: '迪斯科', display_name_en: 'Disco', weight: 3,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Disco', year: '2020-', note: 'Disco 迪斯科球反色' },
grating_config: { sheen_band_angle: 130, sheen_speed: 0.44, sheen_intensity: 0.42, foil_coverage: 0.78, backdrop_tone: '#A89E91' },
color_hint: '彩虹光 多彩',
bg_prompt: 'disco ball holographic surface, scattered facet sparkles across silver base, vivid prismatic points of light, retro disco luxury mood, no text, no people, no watermark',
overlay_prompt: 'silver bevel border, scattered facet sparkle',
tags: 'disco,party',
},
{
id: 'topps_atomic', rarity: 'SR', display_name_zh: '原子折射', display_name_en: 'Topps Atomic', weight: 3,
family: 'topps_chrome',
reference: { series: 'Topps Chrome Atomic Refractor', year: '2018-', note: 'Atomic 原子反色' },
grating_config: { sheen_band_angle: 120, sheen_speed: 0.42, sheen_intensity: 0.40, foil_coverage: 0.76, backdrop_tone: '#A8ACB2' },
color_hint: '电蓝 辐射光',
bg_prompt: 'atomic refractor holographic surface, vivid electric blue and white atomic burst sheen, sharp radiant diagonal sweep, retro sci-fi card mood, no text, no people, no watermark',
overlay_prompt: 'clean silver border, atomic burst corner motif',
tags: 'atomic,blue,sport',
},
{
id: 'hyper_prizm', rarity: 'SR', display_name_zh: '极速棱镜', display_name_en: 'Hyper Prizm', weight: 2,
family: 'panini_prizm',
reference: { series: 'Panini Hyper Prizm', year: '2022-', note: 'Hyper 极速棱镜反色' },
grating_config: { sheen_band_angle: 100, sheen_speed: 0.50, sheen_intensity: 0.42, foil_coverage: 0.80, backdrop_tone: '#9C9993' },
color_hint: '极速银 锐利光',
bg_prompt: 'hyper speed holographic surface, sharp high-speed diagonal sheen sweep across silver base, kinetic energy lines, futuristic racing card mood, no text, no people, no watermark',
overlay_prompt: 'sharp silver border, kinetic corner motif',
tags: 'hyper,speed',
},
{
id: 'cracked_ice', rarity: 'SR', display_name_zh: '碎冰', display_name_en: 'Cracked Ice', weight: 3,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Cracked Ice', year: '2021-', note: 'Cracked Ice 碎冰反色' },
grating_config: { sheen_band_angle: 105, sheen_speed: 0.40, sheen_intensity: 0.38, foil_coverage: 0.74, backdrop_tone: '#A8ACB2' },
color_hint: '冰蓝 裂纹光',
bg_prompt: 'cracked ice holographic surface, crystalline fracture pattern with cool blue sheen in cracks, frozen lake premium mood, no text, no people, no watermark',
overlay_prompt: 'clean silver border, ice crack corner motif',
tags: 'ice,cracked',
},
{
id: 'yugioh_super', rarity: 'SR', display_name_zh: '游戏王 SR', display_name_en: 'Yu-Gi-Oh Super Rare', weight: 2,
family: 'yugioh',
reference: { series: 'Yu-Gi-Oh Super Rare', year: '2002-', note: '游戏王 SR 全卡面横纹反色' },
grating_config: { sheen_band_angle: 90, sheen_speed: 0.38, sheen_intensity: 0.40, foil_coverage: 0.85, backdrop_tone: '#B0ACA6' },
color_hint: '横纹光 彩虹',
bg_prompt: 'Yu-Gi-Oh Super Rare full-card horizontal stripe holographic, parallel horizontal sheen bands sweeping across entire art, classic TCG anime premium mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card sheen stripe',
tags: 'yugioh,stripe',
},
{
id: 'yugioh_ultra', rarity: 'SR', display_name_zh: '游戏王 UR', display_name_en: 'Yu-Gi-Oh Ultra Rare', weight: 2,
family: 'yugioh',
reference: { series: 'Yu-Gi-Oh Ultra Rare', year: '2002-', note: '游戏王 UR 雪花+横纹混合' },
grating_config: { sheen_band_angle: 90, sheen_speed: 0.40, sheen_intensity: 0.42, foil_coverage: 0.88, backdrop_tone: '#B0ACA6' },
color_hint: '雪花 棱镜',
bg_prompt: 'Yu-Gi-Oh Ultra Rare full-card horizontal stripe holographic with cross-hatch sparkles, dense sheen sweep with sparkle dots, premium anime TCG mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card sheen with sparkle dots',
tags: 'yugioh,stripe,sparkle',
},
{
id: 'ws_sr', rarity: 'SR', display_name_zh: 'WS SR', display_name_en: 'Weiss Schwarz SR', weight: 2,
family: 'weiss',
reference: { series: 'Weiss Schwarz SR', year: '2008-', note: 'WS SR 横纹反色' },
grating_config: { sheen_band_angle: 95, sheen_speed: 0.40, sheen_intensity: 0.40, foil_coverage: 0.82, backdrop_tone: '#A8ACB2' },
color_hint: '横纹银 轻彩虹',
bg_prompt: 'Weiss Schwarz SR full-card horizontal stripe holographic, soft parallel silver sheen bands, clean Japanese-anime card premium mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card horizontal sheen',
tags: 'weiss,stripe',
},
// ============ SSR 档(9 个,Full Art/Rainbow/Gold 类稀有工艺)============
{
id: 'rainbow_rare', rarity: 'SSR', display_name_zh: '彩虹稀有', display_name_en: 'Rainbow Rare', weight: 1,
family: 'pokemon_tcg',
reference: { series: 'Pokémon TCG Rainbow Rare', year: '2017-', note: '宝可梦彩虹稀有,真卡顶级工艺' },
grating_config: { sheen_band_angle: 100, sheen_speed: 0.46, sheen_intensity: 0.46, foil_coverage: 0.90, backdrop_tone: '#B0ACA6' },
color_hint: '全光谱彩虹',
bg_prompt: 'Pokémon Rainbow Rare full-card rainbow holographic, dense full-spectrum sheen sweeping across entire art, vivid prismatic layers, premium Japanese TCG luxury mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card rainbow sheen with sparkle dots',
tags: 'pokemon,rainbow,fullart',
},
{
id: 'prizm_gold', rarity: 'SSR', display_name_zh: '金棱镜', display_name_en: 'Prizm Gold', weight: 0.8,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Gold Prizm', year: '2018-', note: '金棱镜,真卡高阶平行版' },
grating_config: { sheen_band_angle: 120, sheen_speed: 0.42, sheen_intensity: 0.42, foil_coverage: 0.85, backdrop_tone: '#A89E91' },
color_hint: '金色 暖金',
bg_prompt: 'gold Prizm holographic surface, vivid gold-to-amber sheen sweeping diagonally, warm metallic premium sport card luxury mood, no text, no people, no watermark',
overlay_prompt: 'gold bevel border, sharp diagonal sheen sweep',
tags: 'gold,sport,prizm',
},
{
id: 'prizm_black', rarity: 'SSR', display_name_zh: '黑棱镜', display_name_en: 'Prizm Black', weight: 0.8,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Black Prizm', year: '2020-', note: '黑棱镜,稀缺平行版' },
grating_config: { sheen_band_angle: 115, sheen_speed: 0.40, sheen_intensity: 0.44, foil_coverage: 0.88, backdrop_tone: '#9C9993' },
color_hint: '黑银 烟雾光',
bg_prompt: 'black Prizm holographic surface, dark silver-to-black gradient with vivid rainbow sparkle dots, mysterious dark luxury card mood, no text, no people, no watermark',
overlay_prompt: 'dark silver border, scattered rainbow sparkles',
tags: 'black,sport,prizm',
},
{
id: 'cosmic_prizm', rarity: 'SSR', display_name_zh: '宇宙棱镜', display_name_en: 'Cosmic Prizm', weight: 0.8,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Cosmic', year: '2023-', note: '宇宙棱镜,空间主题' },
grating_config: { sheen_band_angle: 105, sheen_speed: 0.44, sheen_intensity: 0.44, foil_coverage: 0.86, backdrop_tone: '#A8ACB2' },
color_hint: '深空紫 星光',
bg_prompt: 'cosmic Prizm holographic surface, deep purple and navy gradient with starfield sparkle dots and nebula-like color swirl, cinematic space premium mood, no text, no people, no watermark',
overlay_prompt: 'silver border, star scatter corner',
tags: 'cosmic,space',
},
{
id: 'prizm_shattered', rarity: 'SSR', display_name_zh: '碎光棱镜', display_name_en: 'Shattered Prizm', weight: 0.8,
family: 'panini_prizm',
reference: { series: 'Panini Prizm Shattered Glass', year: '2019-', note: '碎光玻璃反色' },
grating_config: { sheen_band_angle: 95, sheen_speed: 0.42, sheen_intensity: 0.44, foil_coverage: 0.86, backdrop_tone: '#A8ACB2' },
color_hint: '玻璃碎光 棱镜',
bg_prompt: 'shattered glass Prizm holographic surface, geometric glass-shard pattern with prismatic rainbow between shards, dramatic luxury sport card mood, no text, no people, no watermark',
overlay_prompt: 'silver bevel border, glass shard corner motif',
tags: 'shattered,glass',
},
{
id: 'topps_sepia', rarity: 'SSR', display_name_zh: '复古棕', display_name_en: 'Topps Sepia', weight: 0.7,
family: 'topps_chrome',
reference: { series: 'Topps Chrome Sepia Refractor', year: '2017-', note: '复古棕反色,棒球稀有平行' },
grating_config: { sheen_band_angle: 130, sheen_speed: 0.38, sheen_intensity: 0.40, foil_coverage: 0.82, backdrop_tone: '#A89E91' },
color_hint: '棕褐色 复古',
bg_prompt: 'sepia refractor holographic surface, warm brown and amber gradient with vintage metallic sheen, nostalgic baseball-card premium mood, no text, no people, no watermark',
overlay_prompt: 'warm sepia border, vintage corner motif',
tags: 'sepia,vintage,sport',
},
{
id: 'pokemon_fullart', rarity: 'SSR', display_name_zh: '全图宝可梦', display_name_en: 'Pokémon Full Art', weight: 0.7,
family: 'pokemon_tcg',
reference: { series: 'Pokémon TCG Full Art Rare', year: '2010-', note: '宝可梦全图稀有' },
grating_config: { sheen_band_angle: 110, sheen_speed: 0.44, sheen_intensity: 0.44, foil_coverage: 0.88, backdrop_tone: '#A8ACB2' },
color_hint: '柔光 全图',
bg_prompt: 'Pokémon Full Art holographic background with edge-to-edge soft rainbow sheen, art fills entire card with shimmering border halo, premium Japanese TCG mood, no text, no people, no watermark',
overlay_prompt: 'soft rainbow halo around art, no hard border',
tags: 'pokemon,fullart',
},
{
id: 'yugioh_secret', rarity: 'SSR', display_name_zh: '游戏王 Secret', display_name_en: 'Yu-Gi-Oh Secret Rare', weight: 0.7,
family: 'yugioh',
reference: { series: 'Yu-Gi-Oh Secret Rare', year: '2002-', note: '游戏王 Secret 横纹+名字彩虹' },
grating_config: { sheen_band_angle: 90, sheen_speed: 0.44, sheen_intensity: 0.44, foil_coverage: 0.90, backdrop_tone: '#B0ACA6' },
color_hint: '横纹彩虹 Secret',
bg_prompt: 'Yu-Gi-Oh Secret Rare full-card horizontal stripe holographic with sparkling diagonal cross-sheen, premium anime TCG secret-tier mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card sparkle cross-sheen',
tags: 'yugioh,secret',
},
{
id: 'gold_secret', rarity: 'SSR', display_name_zh: '游戏王金 Secret', display_name_en: 'Yu-Gi-Oh Gold Secret', weight: 0.7,
family: 'yugioh',
reference: { series: 'Yu-Gi-Oh Gold Secret Rare', year: '2012-', note: '游戏王金 Secret 稀有' },
grating_config: { sheen_band_angle: 90, sheen_speed: 0.42, sheen_intensity: 0.42, foil_coverage: 0.92, backdrop_tone: '#A89E91' },
color_hint: '金色 Secret',
bg_prompt: 'Yu-Gi-Oh Gold Secret full-card warm gold horizontal stripe holographic, dense gold sheen sweeping across entire art, premium anime TCG luxury mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card gold sheen',
tags: 'yugioh,gold',
},
// ============ UR 档(6 个,终极稀有)============
{
id: 'pokemon_star', rarity: 'UR', display_name_zh: '宝可梦星卡', display_name_en: 'Pokémon Star', weight: 0.4,
family: 'pokemon_tcg',
reference: { series: 'Pokémon TCG Star Holo', year: '2000-2003', note: '宝可梦初代星卡,真卡传奇工艺' },
grating_config: { sheen_band_angle: 95, sheen_speed: 0.50, sheen_intensity: 0.50, foil_coverage: 0.95, backdrop_tone: '#B0ACA6' },
color_hint: '全光谱 星辉',
bg_prompt: 'Pokémon Star holographic surface with five-point star burst pattern across entire art, intense prismatic star radiance, legendary premium TCG mood, no text, no people, no watermark',
overlay_prompt: 'star burst overlay across entire art',
tags: 'pokemon,star,legend',
},
{
id: 'superfractor', rarity: 'UR', display_name_zh: '至尊折射', display_name_en: 'Topps Superfractor', weight: 0.3,
family: 'topps_chrome',
reference: { series: 'Topps Chrome Superfractor (1/1)', year: '1996-', note: 'Topps Chrome 1/1 真卡顶点' },
grating_config: { sheen_band_angle: 100, sheen_speed: 0.50, sheen_intensity: 0.55, foil_coverage: 1.00, backdrop_tone: '#B0ACA6' },
color_hint: '全光谱彩虹',
bg_prompt: 'Topps Chrome Superfractor 1/1 full-spectrum rainbow holographic, intense prismatic rainbow burst across entire art, ultimate luxury card apex mood, no text, no people, no watermark',
overlay_prompt: 'rainbow burst overlay, no border',
tags: 'superfractor,1of1,apex',
},
{
id: 'yugioh_starlight', rarity: 'UR', display_name_zh: '游戏王星光', display_name_en: 'Yu-Gi-Oh Starlight Rare', weight: 0.3,
family: 'yugioh',
reference: { series: 'Yu-Gi-Oh Starlight Rare', year: '2018-', note: '游戏王星光稀有,终极工艺' },
grating_config: { sheen_band_angle: 95, sheen_speed: 0.48, sheen_intensity: 0.50, foil_coverage: 0.98, backdrop_tone: '#B0ACA6' },
color_hint: '星光 全光谱',
bg_prompt: 'Yu-Gi-Oh Starlight Rare full-card starlight burst holographic, intense star-pattern radiance across entire art, ultimate anime TCG apex mood, no text, no people, no watermark',
overlay_prompt: 'starlight burst overlay, no border',
tags: 'yugioh,starlight,apex',
},
{
id: 'yugioh_platinum', rarity: 'UR', display_name_zh: '游戏王白金', display_name_en: 'Yu-Gi-Oh Platinum Secret', weight: 0.3,
family: 'yugioh',
reference: { series: 'Yu-Gi-Oh Platinum Secret Rare', year: '2014-', note: '游戏王白金 Secret' },
grating_config: { sheen_band_angle: 90, sheen_speed: 0.46, sheen_intensity: 0.50, foil_coverage: 0.98, backdrop_tone: '#B0ACA6' },
color_hint: '白金 棱镜',
bg_prompt: 'Yu-Gi-Oh Platinum Secret full-card platinum-white horizontal stripe holographic with cross sparkle, intense white-silver sheen, ultimate anime TCG mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card platinum sheen',
tags: 'yugioh,platinum',
},
{
id: 'yugioh_prismatic', rarity: 'UR', display_name_zh: '游戏王棱镜 Secret', display_name_en: 'Yu-Gi-Oh Prismatic Secret', weight: 0.3,
family: 'yugioh',
reference: { series: 'Yu-Gi-Oh Prismatic Secret Rare', year: '2020-', note: '游戏王棱镜 Secret' },
grating_config: { sheen_band_angle: 90, sheen_speed: 0.46, sheen_intensity: 0.52, foil_coverage: 0.98, backdrop_tone: '#B0ACA6' },
color_hint: '棱镜 彩虹',
bg_prompt: 'Yu-Gi-Oh Prismatic Secret full-card prismatic rainbow with multi-axis sheen, intense multi-directional prismatic burst, ultimate anime TCG apex mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card prismatic burst',
tags: 'yugioh,prismatic',
},
{
id: 'ws_ssp', rarity: 'UR', display_name_zh: 'WS SSP', display_name_en: 'Weiss SSP', weight: 0.3,
family: 'weiss',
reference: { series: 'Weiss Schwarz SSP', year: '2015-', note: 'WS SSP 终极签名/限定' },
grating_config: { sheen_band_angle: 95, sheen_speed: 0.48, sheen_intensity: 0.48, foil_coverage: 0.95, backdrop_tone: '#A8ACB2' },
color_hint: 'SSP 彩虹',
bg_prompt: 'Weiss Schwarz SSP full-card horizontal stripe holographic with rainbow sparkle dots, dense premium anime collectible apex mood, no text, no people, no watermark',
overlay_prompt: 'no border, full-card sparkle stripe',
tags: 'weiss,ssp,apex',
},
]
/** 全池 style ID 列表(顺序与 LASER_STYLE_POOL 一致) */
export const LASER_STYLE_IDS = LASER_STYLE_POOL.map((s) => s.id)
/** O(1) ID 索引(用于通过 preset_id 反查 style) */
export const LASER_STYLE_INDEX = LASER_STYLE_POOL.reduce(
(acc, s, i) => {
acc[s.id] = i
return acc
},
/** @type {Record<string, number>} */ ({}),
)
/** 档位统计({N:10, R:10, SR:10, SSR:9, UR:6}) */
export const RARITY_DISTRIBUTION = LASER_STYLE_POOL.reduce(
(acc, s) => {
acc[s.rarity] = (acc[s.rarity] || 0) + 1
return acc
},
/** @type {Record<string, number>} */ ({}),
)
/** 档位配色(供 UI 徽章复用) */
export const RARITY_COLORS = {
N: '#78828C',
R: '#50A0DC',
SR: '#B464DC',
SSR: 'linear-gradient(90deg, #ff8a00, #e52e71)',
UR: 'linear-gradient(90deg, #ff5e62, #ff9966, #ff5e62)',
}
/** 档位星数(供 UI 显示) */
export const RARITY_STARS = {
N: '★',
R: '★★',
SR: '★★★',
SSR: '★★★★',
UR: '★★★★★',
}