diff --git a/AGENTS.md b/AGENTS.md index bd8bc69..2ca2ceb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ Git 历史采用 Conventional Commits,且摘要多为中文,例如 `feat: # Memory Context -# $CMEM mp-xieyingeng 2026-05-03 10:02pm GMT+8 +# $CMEM mp-xieyingeng 2026-05-03 10:24pm GMT+8 Legend: 🎯session 🔴bugfix 🟣feature 🔄refactor ✅change 🔵discovery ⚖️decision Format: ID TIME TYPE TITLE diff --git a/assets/prefabs/PageLevel.prefab b/assets/prefabs/PageLevel.prefab index 72519db..5c44281 100644 --- a/assets/prefabs/PageLevel.prefab +++ b/assets/prefabs/PageLevel.prefab @@ -743,8 +743,8 @@ }, "_lpos": { "__type__": "cc.Vec3", - "x": 8.006, - "y": 2.227, + "x": -4.854, + "y": -3.284, "z": 0 }, "_lrot": { @@ -756,9 +756,9 @@ }, "_lscale": { "__type__": "cc.Vec3", - "x": 1.0000000000000002, - "y": 1.0000000000000002, - "z": 0.759 + "x": 0.92, + "y": 0.92, + "z": 0.698 }, "_mobility": 0, "_layer": 1073741824, @@ -1051,7 +1051,7 @@ "node": { "__id__": 25 }, - "_enabled": true, + "_enabled": false, "__prefab": { "__id__": 41 }, @@ -1069,7 +1069,7 @@ "__uuid__": "02687761-81ab-42cb-b552-0db291d9ddd5@f9941", "__expectedType__": "cc.SpriteFrame" }, - "_type": 0, + "_type": 1, "_fillType": 0, "_sizeMode": 0, "_fillCenter": { @@ -1318,7 +1318,7 @@ "_lpos": { "__type__": "cc.Vec3", "x": 0, - "y": -5.11, + "y": 0.401, "z": 0 }, "_lrot": { @@ -1330,9 +1330,9 @@ }, "_lscale": { "__type__": "cc.Vec3", - "x": 1.0000000000000002, - "y": 1.0000000000000002, - "z": 0.759 + "x": 0.92, + "y": 0.92, + "z": 0.698 }, "_mobility": 0, "_layer": 1073741824, @@ -1625,7 +1625,7 @@ "node": { "__id__": 49 }, - "_enabled": true, + "_enabled": false, "__prefab": { "__id__": 65 }, @@ -1643,7 +1643,7 @@ "__uuid__": "02687761-81ab-42cb-b552-0db291d9ddd5@f9941", "__expectedType__": "cc.SpriteFrame" }, - "_type": 0, + "_type": 1, "_fillType": 0, "_sizeMode": 0, "_fillCenter": { @@ -7939,7 +7939,6 @@ "titleLevelLabel": { "__id__": 14 }, - "currentLevelIndex": 0, "clickAudio": { "__uuid__": "a68a6314-fb7c-48a9-bd6c-0a65ef665d50", "__expectedType__": "cc.AudioClip" @@ -7964,6 +7963,11 @@ "__uuid__": "e41c722f-f605-47f7-9ce4-abff0ed2020f", "__expectedType__": "cc.Prefab" }, + "roundedSpriteEffect": { + "__uuid__": "f0080a34-1786-4547-8d81-d89cc517b63e", + "__expectedType__": "cc.EffectAsset" + }, + "mainImageCornerRadius": 0.1, "_id": "" }, { diff --git a/assets/prefabs/PageLevel.ts b/assets/prefabs/PageLevel.ts index e5a590e..c863a37 100644 --- a/assets/prefabs/PageLevel.ts +++ b/assets/prefabs/PageLevel.ts @@ -1,4 +1,4 @@ -import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, Prefab } from 'cc'; +import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, Prefab, EffectAsset, UITransform } from 'cc'; import { BaseView } from 'db://assets/scripts/core/BaseView'; import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { StaminaManager } from 'db://assets/scripts/utils/StaminaManager'; @@ -13,6 +13,7 @@ import { WrongModal } from 'db://assets/prefabs/WrongModal'; import { TimeoutModal } from 'db://assets/prefabs/TimeoutModal'; import { StaminaInfo, NextLevelData } from 'db://assets/scripts/types/ApiTypes'; import { AchievementTitleManager } from 'db://assets/scripts/utils/AchievementTitleManager'; +import { applyRoundedCorner } from 'db://assets/scripts/utils/roundedMaterial.utils'; const { ccclass, property } = _decorator; /** @@ -37,6 +38,9 @@ export class PageLevel extends BaseView { /** 答案正确后展示包袱答案的停留时间 */ private static readonly PASS_MODAL_DELAY_MS = 2000; + /** 图片2描述默认文案 */ + private static readonly DEFAULT_IMAGE2_DESCRIPTION = '这是什么?'; + // ========== 节点引用 ========== @property(Node) inputLayout: Node | null = null; @@ -116,6 +120,14 @@ export class PageLevel extends BaseView { @property(Prefab) timeoutModalPrefab: Prefab | null = null; + /** 主图圆角材质 EffectAsset */ + @property(EffectAsset) + roundedSpriteEffect: EffectAsset | null = null; + + /** 主图圆角半径比例(相对于短边,0-0.5) */ + @property + mainImageCornerRadius: number = 0.1; + // ========== 内部状态 ========== /** 当前创建的输入框节点数组 */ private _inputNodes: Node[] = []; @@ -814,6 +826,7 @@ export class PageLevel extends BaseView { const sprite = this.mainImage.getComponent(Sprite); if (sprite && spriteFrame) { sprite.spriteFrame = spriteFrame; + this.applyMainImageRoundedCorner(sprite); console.log('[PageLevel] 设置主图1'); } } @@ -827,10 +840,30 @@ export class PageLevel extends BaseView { const sprite = this.mainImage2.getComponent(Sprite); if (sprite && spriteFrame) { sprite.spriteFrame = spriteFrame; + this.applyMainImageRoundedCorner(sprite); console.log('[PageLevel] 设置主图2'); } } + private applyMainImageRoundedCorner(sprite: Sprite): void { + if (!this.roundedSpriteEffect) { + return; + } + + const uiTransform = sprite.node.getComponent(UITransform); + if (!uiTransform) { + return; + } + + applyRoundedCorner( + sprite, + this.roundedSpriteEffect, + uiTransform.width, + uiTransform.height, + this.mainImageCornerRadius + ); + } + /** * 设置图片描述文本 */ @@ -839,7 +872,7 @@ export class PageLevel extends BaseView { this.image1DescLabel.string = desc1 ?? ''; } if (this.image2DescLabel) { - this.image2DescLabel.string = desc2 ?? ''; + this.image2DescLabel.string = desc2?.trim() ? desc2 : PageLevel.DEFAULT_IMAGE2_DESCRIPTION; } } diff --git a/assets/prefabs/PassModal.prefab b/assets/prefabs/PassModal.prefab index d977424..f66b261 100644 --- a/assets/prefabs/PassModal.prefab +++ b/assets/prefabs/PassModal.prefab @@ -546,7 +546,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 1297, + "width": 1299, "height": 1004 }, "_anchorPoint": { @@ -583,7 +583,7 @@ "a": 255 }, "_spriteFrame": { - "__uuid__": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@f9941", + "__uuid__": "f8c208df-8e8c-456e-9f97-1d1c07518452@f9941", "__expectedType__": "cc.SpriteFrame" }, "_type": 0, diff --git a/assets/resources/effects.meta b/assets/resources/effects.meta new file mode 100644 index 0000000..ecf8765 --- /dev/null +++ b/assets/resources/effects.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "5a2048d4-99ea-4f02-99b3-2ba9d1ec4fd5", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/resources/effects/rounded-sprite.effect b/assets/resources/effects/rounded-sprite.effect new file mode 100644 index 0000000..9f67e3f --- /dev/null +++ b/assets/resources/effects/rounded-sprite.effect @@ -0,0 +1,140 @@ +// Rounded Corner Sprite Effect +// 用于实现圆角矩形裁剪效果,性能优于 Mask 方案 +CCEffect %{ +techniques: + - passes: + - vert: sprite-vs:vert + frag: sprite-fs:frag + depthStencilState: + depthTest: false + depthWrite: false + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendSrcAlpha: src_alpha + blendDstAlpha: one_minus_src_alpha + rasterizerState: + cullMode: none + properties: + alphaThreshold: { value: 0.5 } + # 圆角参数: x=圆角半径比例(0-0.5), y=预留, z=节点宽度, w=节点高度 + roundedParams: { value: [0.08, 0.0, 347.0, 200.0] } + # UV 区域: x=minU, y=minV, z=rangeU, w=rangeV (用于裁剪后的 SpriteFrame 归一化) + uvRect: { value: [0.0, 0.0, 1.0, 1.0] } +}% + +CCProgram sprite-vs %{ +precision highp float; +#include +#if USE_LOCAL + #include +#endif +#if SAMPLE_FROM_RT + #include +#endif +in vec3 a_position; +in vec2 a_texCoord; +in vec4 a_color; + +out vec4 color; +out vec2 uv0; + +vec4 vert() { + vec4 pos = vec4(a_position, 1); + + #if USE_LOCAL + pos = cc_matWorld * pos; + #endif + + #if USE_PIXEL_ALIGNMENT + pos = cc_matView * pos; + pos.xyz = floor(pos.xyz); + pos = cc_matProj * pos; + #else + pos = cc_matViewProj * pos; + #endif + + uv0 = a_texCoord; + #if SAMPLE_FROM_RT + CC_HANDLE_RT_SAMPLE_FLIP(uv0); + #endif + color = a_color; + + return pos; +} +}% + +CCProgram sprite-fs %{ +precision highp float; +#include +#include + +in vec4 color; +in vec2 uv0; + +// roundedParams: x=圆角半径比例, y=预留, z=节点宽度, w=节点高度 +// uvRect: x=minU, y=minV, z=rangeU, w=rangeV (裁剪后的 SpriteFrame UV 归一化) +uniform Custom { + vec4 roundedParams; + vec4 uvRect; +}; + +#if USE_TEXTURE + #pragma builtin(local) + layout(set = 2, binding = 12) uniform sampler2D cc_spriteTexture; +#endif + +// 计算点到圆角矩形边界的有符号距离 +float roundedBoxSDF(vec2 centerPos, vec2 halfSize, float radius) { + vec2 q = abs(centerPos) - halfSize + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; +} + +vec4 frag() { + vec4 o = vec4(1, 1, 1, 1); + + #if USE_TEXTURE + o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0); + #endif + + o *= color; + + // 解析参数 + float cornerRadius = roundedParams.x; + vec2 nodeSize = vec2(roundedParams.z, roundedParams.w); + + // 将实际 UV 归一化到 0→1 范围(兼容裁剪后的 SpriteFrame) + // uvRect: (minU, minV, rangeU, rangeV) + vec2 normalizedUV = (uv0 - uvRect.xy) / uvRect.zw; + + // 将归一化 UV 转换为以中心为原点的坐标系 + vec2 centerUV = normalizedUV - 0.5; + + // 计算实际的圆角半径(基于短边) + float minSize = min(nodeSize.x, nodeSize.y); + float radius = cornerRadius * minSize; + + // 将 UV 坐标转换为像素坐标 + vec2 pixelPos = centerUV * nodeSize; + vec2 halfSize = nodeSize * 0.5; + + // 计算 SDF 距离 + float dist = roundedBoxSDF(pixelPos, halfSize, radius); + + // 使用平滑的抗锯齿边缘 + float smoothEdge = 1.0 - smoothstep(-1.0, 1.0, dist); + + o.a *= smoothEdge; + + // 灰度转换(与 Cocos 内置 sprite shader 一致的 luminance 权重) + #if USE_GRAY_SCALE + float gray = dot(o.rgb, vec3(0.2126, 0.7152, 0.0722)); + o.rgb = vec3(gray); + #endif + + ALPHA_TEST(o); + return o; +} +}% diff --git a/assets/resources/effects/rounded-sprite.effect.meta b/assets/resources/effects/rounded-sprite.effect.meta new file mode 100644 index 0000000..44ad187 --- /dev/null +++ b/assets/resources/effects/rounded-sprite.effect.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.7.1", + "importer": "effect", + "imported": true, + "uuid": "f0080a34-1786-4547-8d81-d89cc517b63e", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/assets/resources/images/ip/PicWin.png b/assets/resources/images/ip/PicWin.png new file mode 100644 index 0000000..f76b1a6 Binary files /dev/null and b/assets/resources/images/ip/PicWin.png differ diff --git a/assets/resources/images/pageLevel/1_0022_Layer-19.png.meta b/assets/resources/images/ip/PicWin.png.meta similarity index 76% rename from assets/resources/images/pageLevel/1_0022_Layer-19.png.meta rename to assets/resources/images/ip/PicWin.png.meta index f3b1f79..1eea735 100644 --- a/assets/resources/images/pageLevel/1_0022_Layer-19.png.meta +++ b/assets/resources/images/ip/PicWin.png.meta @@ -2,7 +2,7 @@ "ver": "1.0.27", "importer": "image", "imported": true, - "uuid": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400", + "uuid": "f8c208df-8e8c-456e-9f97-1d1c07518452", "files": [ ".json", ".png" @@ -10,14 +10,14 @@ "subMetas": { "6c48a": { "importer": "texture", - "uuid": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@6c48a", - "displayName": "1_0022_Layer-19", + "uuid": "f8c208df-8e8c-456e-9f97-1d1c07518452@6c48a", + "displayName": "PicWin", "id": "6c48a", "name": "texture", "userData": { "wrapModeS": "clamp-to-edge", "wrapModeT": "clamp-to-edge", - "imageUuidOrDatabaseUri": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400", + "imageUuidOrDatabaseUri": "f8c208df-8e8c-456e-9f97-1d1c07518452", "isUuid": true, "visible": false, "minfilter": "linear", @@ -34,8 +34,8 @@ }, "f9941": { "importer": "sprite-frame", - "uuid": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@f9941", - "displayName": "1_0022_Layer-19", + "uuid": "f8c208df-8e8c-456e-9f97-1d1c07518452@f9941", + "displayName": "PicWin", "id": "f9941", "name": "spriteFrame", "userData": { @@ -45,9 +45,9 @@ "offsetY": 0, "trimX": 0, "trimY": 0, - "width": 1297, + "width": 1299, "height": 1004, - "rawWidth": 1297, + "rawWidth": 1299, "rawHeight": 1004, "borderTop": 0, "borderBottom": 0, @@ -60,16 +60,16 @@ "meshType": 0, "vertices": { "rawPosition": [ - -648.5, + -649.5, -502, 0, - 648.5, + 649.5, -502, 0, - -648.5, + -649.5, 502, 0, - 648.5, + 649.5, 502, 0 ], @@ -84,11 +84,11 @@ "uv": [ 0, 1004, - 1297, + 1299, 1004, 0, 0, - 1297, + 1299, 0 ], "nuv": [ @@ -102,18 +102,18 @@ 1 ], "minPos": [ - -648.5, + -649.5, -502, 0 ], "maxPos": [ - 648.5, + 649.5, 502, 0 ] }, "isUuid": true, - "imageUuidOrDatabaseUri": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@6c48a", + "imageUuidOrDatabaseUri": "f8c208df-8e8c-456e-9f97-1d1c07518452@6c48a", "atlasUuid": "", "trimType": "auto" }, @@ -129,6 +129,6 @@ "type": "sprite-frame", "fixAlphaTransparencyArtifacts": false, "hasAlpha": true, - "redirect": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@6c48a" + "redirect": "f8c208df-8e8c-456e-9f97-1d1c07518452@6c48a" } } diff --git a/assets/resources/images/pageLevel/1_0022_Layer-19.png b/assets/resources/images/pageLevel/1_0022_Layer-19.png deleted file mode 100644 index 5039f77..0000000 Binary files a/assets/resources/images/pageLevel/1_0022_Layer-19.png and /dev/null differ diff --git a/assets/resources/images/pageLevel/PicLayer.png.meta b/assets/resources/images/pageLevel/PicLayer.png.meta index fd3992e..006ad7e 100644 --- a/assets/resources/images/pageLevel/PicLayer.png.meta +++ b/assets/resources/images/pageLevel/PicLayer.png.meta @@ -49,10 +49,10 @@ "height": 1189, "rawWidth": 1190, "rawHeight": 1189, - "borderTop": 0, - "borderBottom": 0, - "borderLeft": 0, - "borderRight": 0, + "borderTop": 220, + "borderBottom": 220, + "borderLeft": 220, + "borderRight": 220, "packable": true, "pixelsToUnit": 100, "pivotX": 0.5, diff --git a/assets/scripts/utils/roundedMaterial.utils.ts b/assets/scripts/utils/roundedMaterial.utils.ts new file mode 100644 index 0000000..17acecc --- /dev/null +++ b/assets/scripts/utils/roundedMaterial.utils.ts @@ -0,0 +1,99 @@ +import { EffectAsset, Material, Sprite, Vec4 } from 'cc'; + +/** + * 圆角 Sprite 材质工具。 + * + * 基于 rounded-sprite.effect 的 SDF alpha 裁剪实现,直接作用于 Sprite 的自定义材质。 + * 每个 Sprite 都会获得独立 Material,避免 roundedParams / uvRect 互相覆盖。 + */ + +let cachedTemplate: Material | null = null; +let cachedEffectRef: EffectAsset | null = null; + +const getOrCreateTemplate = (effectAsset: EffectAsset): Material => { + if (cachedTemplate && cachedEffectRef === effectAsset) { + return cachedTemplate; + } + + const template = new Material(); + template.initialize({ + effectAsset, + defines: { USE_TEXTURE: true }, + }); + + cachedTemplate = template; + cachedEffectRef = effectAsset; + + return template; +}; + +const extractUvRect = (sprite: Sprite): Vec4 => { + const spriteFrame = sprite.spriteFrame; + if (!spriteFrame) { + return new Vec4(0, 0, 1, 1); + } + + const uv = spriteFrame.uv; + if (!uv || uv.length < 8) { + return new Vec4(0, 0, 1, 1); + } + + const u0 = uv[0]; + const v0 = uv[1]; + const u1 = uv[2]; + const v1 = uv[3]; + const u2 = uv[4]; + const v2 = uv[5]; + const u3 = uv[6]; + const v3 = uv[7]; + + const minU = Math.min(u0, u1, u2, u3); + const maxU = Math.max(u0, u1, u2, u3); + const minV = Math.min(v0, v1, v2, v3); + const maxV = Math.max(v0, v1, v2, v3); + const rangeU = maxU - minU; + const rangeV = maxV - minV; + + if (rangeU <= 0 || rangeV <= 0) { + return new Vec4(0, 0, 1, 1); + } + + return new Vec4(minU, minV, rangeU, rangeV); +}; + +export const applyRoundedCorner = ( + sprite: Sprite, + effectAsset: EffectAsset, + width: number, + height: number, + cornerRadius = 0.1, + grayscale = false, +): void => { + if (!sprite || !sprite.isValid) { + console.warn('[roundedMaterial] Invalid sprite, skipping'); + return; + } + + if (!effectAsset) { + console.warn('[roundedMaterial] EffectAsset is null, skipping'); + return; + } + + const template = getOrCreateTemplate(effectAsset); + const materialInstance = new Material(); + const defines: Record = { USE_TEXTURE: true }; + if (grayscale) { + defines.IS_GRAY = true; + } + + materialInstance.initialize({ + effectAsset, + defines, + }); + + const params = new Vec4(cornerRadius, 0, width, height); + materialInstance.setProperty('roundedParams', params); + materialInstance.setProperty('uvRect', extractUvRect(sprite)); + + sprite.customMaterial = materialInstance; +}; diff --git a/assets/scripts/utils/roundedMaterial.utils.ts.meta b/assets/scripts/utils/roundedMaterial.utils.ts.meta new file mode 100644 index 0000000..b0d422b --- /dev/null +++ b/assets/scripts/utils/roundedMaterial.utils.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "216272d4-0b46-425b-be95-28e46f1417f4", + "files": [], + "subMetas": {}, + "userData": {} +}