Skip to content

自定义角色/克隆体的模糊特效 #2195

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 303 additions & 0 deletions extensions/KongMing/blur.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
(function(sc) {

Check failure on line 1 in extensions/KongMing/blur.js

View workflow job for this annotation

GitHub Actions / lint

Use the function form of 'use strict'
// 使用Map对象存储缓存,性能更好
const blurCache = new Map()

// 创建SVG皮肤并处理加载回调
function createSVGSkin(svgData, roX, roY) {
const skinId = runtime.renderer.createSVGSkin(svgData)
if (!skinId) return null

const svgSkin = runtime.renderer._allSkins[skinId]

// 确保立即设置旋转中心
svgSkin.rotationCenter = [roX, roY, 0]

const originalOnLoad = svgSkin._onLoad

svgSkin._onLoad = function() {
if (originalOnLoad) originalOnLoad.call(this)

// 再次确认旋转中心
this.rotationCenter = [roX, roY, 0]
this._svgDirty = true

const drawableIds = runtime.renderer._skinIdToDrawableId[skinId] || []
drawableIds.forEach(drawableId => {
runtime.renderer._allDrawables[drawableId]._skinDirty = true
})
}

return skinId
}

const vm = sc.vm
const runtime = vm.runtime

// 完整多语言设置
const setup = {
'zh-cn': {
'extension_name': '高级模糊特效',
'setBlur': '为我设定[blur]级模糊的[costume]号造型 [cache]缓存',
'setCache': '为[costume]号造型预生成[blurFrom]-[blurTo]级模糊缓存',
'restoreBlur':'恢复造型',
'cache_yes': '使用缓存',
'cache_no': '不使用缓存',
'clear_cache': '清空模糊缓存'
},
'en': {
'extension_name': 'Advanced Blur',
'setBlur': 'Set [costume] number of blur for me [blur] level [cache]',
'setCache': 'Pre-generate blur cache [blurFrom]-[blurTo] for costume [costume]',
'restoreBlur':'Restore costume',
'cache_yes': 'use cache',
'cache_no': 'no cache',
'clear_cache': 'Clear blur cache'
}
}

// 增强的翻译函数
function translate(key, defaultValue, isClone = false) {
const lang = sc.translate.language in setup ? sc.translate.language : 'en'

// 如果是克隆体且有针对克隆体的翻译key,优先使用
if (isClone && key === 'setBlur' && setup[lang]['setBlurForClone']) {
return setup[lang]['setBlurForClone']
}

return setup[lang][key] || defaultValue || key
}

class BlurExtension {
constructor() {
sc.translate.setup(setup)
}

getInfo() {
return {
id: 'blur',
name: translate('extension_name'),
color1:'#668cff',
color2:'#3d6dff',
color3:'#7c9dff',
blockIconURI:'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMjMuMDE4NTciIGhlaWdodD0iNzcuODM1NDEiIHZpZXdCb3g9IjAsMCwxMjMuMDE4NTcsNzcuODM1NDEiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xODAuMjQ3NDcsLTEzNi41ODExNykiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIj48cGF0aCBkPSJNMjE1LjkyMTY0LDE1NS44MjE0OWwtMjguNDI0MTYsMTguNzg4MTUiIHN0cm9rZT0iIzhjOWJmZiIgc3Ryb2tlLXdpZHRoPSIxNC41Ii8+PHBhdGggZD0iTTIxMS4zNzUzNywxOTguOTE1NDlsLTIzLjg3Nzg5LC0yNC4zMDU4NCIgc3Ryb2tlPSIjOGM5YmZmIiBzdHJva2Utd2lkdGg9IjE0LjUiLz48cGF0aCBkPSJNMjExLjM3NTM3LDE5OC45MTU0OWwtMjMuODc3ODksLTI0LjMwNTg0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iNC41Ii8+PHBhdGggZD0iTTIxNS45MjE2NCwxNTUuODIxNDlsLTI4LjQyNDE2LDE4Ljc4ODE1IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iNC41Ii8+PGc+PHBhdGggZD0iTTI1NC40NjkyMSwxNDMuODMxMTdsLTI1LjE0NjMxLDYzLjMzNTQxIiBzdHJva2U9IiM4YzliZmYiIHN0cm9rZS13aWR0aD0iMTQuNSIvPjxwYXRoIGQ9Ik0yNTQuNDY5MjEsMTQzLjgzMTE3bC0yNS4xNDYzMSw2My4zMzU0MSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjQuNSIvPjwvZz48cGF0aCBkPSJNMjY5LjUzMDM2LDE1Ny4yMDg5OWwyNi40ODU2OCwyMS40MzQ0NiIgc3Ryb2tlPSIjOGM5YmZmIiBzdHJva2Utd2lkdGg9IjE0LjUiLz48cGF0aCBkPSJNMjY5LjkxMTQ3LDIwMC41NDA0NWwyNi4xMDQ1OCwtMjEuODk3IiBzdHJva2U9IiM4YzliZmYiIHN0cm9rZS13aWR0aD0iMTQuNSIvPjxwYXRoIGQ9Ik0yNjkuOTExNDcsMjAwLjU0MDQ1bDI2LjEwNDU4LC0yMS44OTciIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSI0LjUiLz48cGF0aCBkPSJNMjY5LjUzMDM2LDE1Ny4yMDg5OWwyNi40ODU2OCwyMS40MzQ0NiIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjQuNSIvPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjU5Ljc1MjUyNDk5OTk5OTk5OjQzLjQxODgzMDAwMDAwMDAxNC0tPg==',
blocks: [
{
opcode: 'setBlur',
blockType: 'command',
text: translate('setBlur', 'Set blur level [blur] for costume [costume]'),
arguments: {
blur: {
type: 'number',
defaultValue: 5
},
costume: {
type: 'number',
defaultValue: 1
},
cache: {
type: 'boolean',
defaultValue: translate('cache_yes'),
menu: 'cache'
}
}
},
{
opcode: 'setCache',
blockType: 'command',
text: translate('setCache'),
arguments: {
costume: {
type: 'number',
defaultValue: 1
},
blurFrom: {
type: 'number',
defaultValue: 0
},
blurTo: {
type: 'number',
defaultValue: 5
}
}
},
{
opcode: 'clearCache',
blockType: 'command',
text: translate('clear_cache')
},
{
opcode: 'restoreBlur',
blockType: 'command',
text: translate('restoreBlur', 'Restore costume'),
}
],
menus: {
cache: {
items: [
{text: translate('cache_yes'), value: true},
{text: translate('cache_no'), value: false}
]
}
}
}
}

setBlur(args, util) {
try {
const target = util.target
let { blur, costume } = args
const useCache = args.cache

if(blur == 0) {
this.restoreBlur(undefined, util)
return
}

blur = Math.ceil(blur)
costume = Math.max(1, Math.min(costume, target.sprite.costumes_.length))

const cacheKey = `${target.sprite.name}_${costume}_${blur}`

if (useCache && blurCache.has(cacheKey)) {
const cached = blurCache.get(cacheKey)
runtime.renderer.updateDrawableSkinId(target.drawableID, cached.skinId)
runtime.requestRedraw()
return
}

// 获取原始SVG和旋转中心
const originalSvgData = target.sprite.costumes_[costume - 1].asset.decodeText()
const roX = target.sprite.costumes_[costume - 1].rotationCenterX
const roY = target.sprite.costumes_[costume - 1].rotationCenterY

const parser = new DOMParser()
const doc = parser.parseFromString(originalSvgData, "image/svg+xml")
const svg = doc.querySelector('svg')
if (!svg) throw new Error("Invalid SVG")

// 确保SVG有正确的viewBox
if (!svg.hasAttribute('viewBox')) {
const bbox = svg.getBBox()
svg.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`)
}

// 创建滤镜
let defs = svg.querySelector('defs') ||
svg.insertBefore(
doc.createElementNS('http://www.w3.org/2000/svg', 'defs'),
svg.firstChild
)

let filter = defs.querySelector('#blurFilter') ||
defs.appendChild(
doc.createElementNS('http://www.w3.org/2000/svg', 'filter')
)
filter.id = 'blurFilter'
filter.setAttribute('x', '-50%')
filter.setAttribute('y', '-50%')
filter.setAttribute('width', '200%')
filter.setAttribute('height', '200%')
filter.setAttribute('filterUnits', 'userSpaceOnUse')

let blurFilter = filter.querySelector('feGaussianBlur') ||
filter.appendChild(
doc.createElementNS('http://www.w3.org/2000/svg', 'feGaussianBlur')
)
blurFilter.setAttribute('stdDeviation', blur)

// 应用滤镜
Array.from(svg.children)
.filter(child => child !== defs)
.forEach(child => child.setAttribute('filter', 'url(#blurFilter)'))

// 创建新皮肤
const newSvgData = new XMLSerializer().serializeToString(svg)
const skinId = createSVGSkin(newSvgData, roX, roY)
if (!skinId) throw new Error("Skin creation failed")

// 更新缓存和渲染
if (useCache) blurCache.set(cacheKey, {skinId, roX, roY})
runtime.renderer.updateDrawableSkinId(target.drawableID, skinId)
runtime.requestRedraw()

} catch (error) {
console.error("Blur error:", error)
}
}

setCache(args, util) {
try {
const target = util.target
let { costume, blurFrom, blurTo } = args

// 验证参数
costume = Math.max(1, Math.min(costume, target.sprite.costumes_.length))
blurFrom = Math.max(0, Math.ceil(blurFrom))
blurTo = Math.max(blurFrom, Math.ceil(blurTo))

// 获取原始SVG
const originalSvgData = target.sprite.costumes_[costume - 1].asset.decodeText()
const parser = new DOMParser()
const doc = parser.parseFromString(originalSvgData, "image/svg+xml")
const svg = doc.querySelector('svg')
if (!svg) throw new Error("Invalid SVG")

// 准备基础SVG结构
let defs = svg.querySelector('defs') ||
svg.insertBefore(
doc.createElementNS('http://www.w3.org/2000/svg', 'defs'),
svg.firstChild
)

let filter = defs.querySelector('#blurFilter') ||
defs.appendChild(
doc.createElementNS('http://www.w3.org/2000/svg', 'filter')
)
filter.id = 'blurFilter'

let blurFilter = filter.querySelector('feGaussianBlur') ||
filter.appendChild(
doc.createElementNS('http://www.w3.org/2000/svg', 'feGaussianBlur')
)

// 应用基础滤镜
Array.from(svg.children)
.filter(child => child !== defs)
.forEach(child => child.setAttribute('filter', 'url(#blurFilter)'))

// 批量生成缓存
for (let i = blurFrom; i <= blurTo; i++) {
blurFilter.setAttribute('stdDeviation', i)
const newSvgData = new XMLSerializer().serializeToString(svg)
const skinId = createSVGSkin(newSvgData)
if (skinId) {
blurCache.set(`${target.sprite.name}_${costume}_${i}`, skinId)
}
}

} catch (error) {
console.error("Pre-cache error:", error)
}
}

clearCache() {
blurCache.clear()
}
restoreBlur(args, util) {
const target = util.target
const drawable = runtime.renderer._allDrawables[target.drawableID]
if (drawable) {
// 重置为原始皮肤
const originalSkinId = target.sprite.costumes_[target.currentCostume].skinId
runtime.renderer.updateDrawableSkinId(target.drawableID, originalSkinId)

// 强制更新属性
drawable._skinDirty = true
runtime.requestRedraw()
}
}
}

// 注册扩展
sc.extensions.register(new BlurExtension())
})(Scratch)
27 changes: 27 additions & 0 deletions extensions/temp/1-我的第一个扩展.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class temp{

Check failure on line 1 in extensions/temp/1-我的第一个扩展.js

View workflow job for this annotation

GitHub Actions / lint

All extension code must be within (function (Scratch) { ... })(Scratch);
//写Scratch扩展时 class里面必须有getInfo方法 如果没有 Scratch会报错
//需要说的是 报错的前提是你把这个没有getInfo方法的class导入到了Scratch里面

getInfo(){
return {
id:'tmep', //id只能是数字或者是字母
name:'我的第一个扩展',

Check failure on line 8 in extensions/temp/1-我的第一个扩展.js

View workflow job for this annotation

GitHub Actions / lint

Extension name should usually be translated
//color1:扩展积木的背景颜色
//color2:扩展积木的边框颜色
//color3:扩展积木输入框的颜色
blocks: /*扩展的积木*/[
{
opcode:'helloWorld',
blockType:'reporter',
text:'hello world'

Check failure on line 16 in extensions/temp/1-我的第一个扩展.js

View workflow job for this annotation

GitHub Actions / lint

Block text should usually be translated
}
]
}
}
//下面就可以自定义积木的方法
//写积木方法的函数名称需要遵循opcode 就是上面的
helloWorld(){
return 'hello world!'
}
}
Scratch.extensions.register(new temp())

Check failure on line 27 in extensions/temp/1-我的第一个扩展.js

View workflow job for this annotation

GitHub Actions / lint

All extension code must be within (function (Scratch) { ... })(Scratch);
Loading
Loading