PageSourceSearch

https://gdbrowser.com/iconkit/icon.js?v=10a

js gdbrowser.com collected 2026-10-02 10:32:14 UTC 28,883 bytes, 747 lines download raw bytes

1const WHITE = 0xffffff
2const colorNames = { "1": "Color 1", "2": "Color 2", "g": "Glow", "w": "White", "u": "UFO Dome" }
3const formNames = { "player": "cube", "player_ball": "ball", "bird": "ufo", "dart": "wave" }
4const extendedFormNames = Object.assign({ icon: "cube", special: "trail", death: "deathEffect", color: "color1" }, formNames)
5const achFormName = { "cube": "icon", "ufo": "bird", "wave": "dart", "color1": "color", "deathEffect": "death", "trail": "special" }
6const specialFormNames = { "trail": "Trail", "deathEffect": "Death Effect", "shipFire": "Ship Fire", "item": "Item" }
7const qualities = { low: 'low', sd: 'low', med: 'hd', medium: 'hd', hd: 'hd' }
8const positionMultipliers = { uhd: 4, hd: 2, low: 1 }
9const yOffsets = { player_ball: -10, bird: 30, spider: 7, swing: -15 }
10const cubeOffsets = {
11    ship: { "x": 0, "y": 5, "scale": 0.55 },
12    bird: { "x": 0, "y": 6, "scale": 0.55 },
13    jetpack: { "x": 3, "y": 2, "scale": 0.6 }
14}
15
16const loadedAssets = {}
17
18async function loadTexture(name, path) {
19    let loaded = await PIXI.Assets.load(path)
20    if (loaded) loadedAssets[name] = loaded
21    return loaded
22}
23
24function downloadFile(data, name) {
25    let url = window.URL.createObjectURL(data);
26    let downloader = document.createElement('a');
27    downloader.href = url
28    downloader.setAttribute("download", name);
29    document.body.appendChild(downloader);
30    downloader.click();
31    document.body.removeChild(downloader);
32}
33
34const loadedIcons = {}
35const iconFrames = {}
36
37function getPositionMultiplier(quality) {
38    return positionMultipliers[quality || 'uhd']
39}
40
41function positionPart(part, partIndex, layer, formName, isGlow, quality) {
42    let positionMultiplier = getPositionMultiplier(quality)
43    layer.position.x += (part.pos[0] * positionMultiplier)
44    layer.position.y -= (part.pos[1] * positionMultiplier)
45    layer.scale.x = part.scale[0]
46    layer.scale.y = part.scale[1]
47    if (part.flipped[0]) layer.scale.x *= -1
48    if (part.flipped[1]) layer.scale.y *= -1
49    layer.angle = part.rotation
50    layer.zIndex = part.z
51
52    if (!isGlow) {
53        let tintInfo = iconStuff.robotAnimations.info[formName].tints
54        let foundTint = tintInfo[partIndex]
55        if (foundTint > 0) {
56            let darkenFilter = new PIXI.ColorMatrixFilter();
57            darkenFilter.brightness(0)
58            darkenFilter.alpha = (255 - foundTint) / 255
59            layer.filters = [darkenFilter]
60        }
61    }
62}
63
64function validNum(val, defaultVal) {
65    let colVal = +val
66    return isNaN(colVal) ? defaultVal : colVal
67}
68
69function getGlowColor(colors) {
70    let glowCol = Number.isInteger(colors["g"]) ? colors["g"] : (colors[2] === 0 ? colors[1] : colors[2])
71    // if (glowCol === 0) glowCol = WHITE // white glow if both colors are black
72    return glowCol
73}
74
75function validateIconID(id, form) {
76    let realID = Math.min(iconStuff.iconCounts[form], Math.abs(validNum(id, 1)))
77    if (realID == 0 && !["player", "player_ball", "ship"].includes(form)) realID = 1
78    return realID
79}
80
81function parseIconColor(col) {
82    if (!col && col != 0) return WHITE
83    else if (typeof col == "string" && col.length == 6) return parseInt(col, 16)
84    let rgb = iconStuff.colors[col]
85    return rgb ? rgbToDecimal(rgb) : WHITE;
86}
87
88function parseIconForm(form, def="player") {
89    let foundForm = iconStuff.forms[form] || iconStuff.forms[formNames]
90    return foundForm ? foundForm.form : def
91}
92
93function loadIconLayers(form, id, cb) {
94    let iconStr = `${form}_${padZero(validateIconID(id, form))}`
95    return loadIconSheet(iconStr, cb)
96}
97
98function loadIconSheet(iconStr, cb) {
99    fetch(`/iconkit/icons/${iconStr}-uhd.plist`).then(pl => pl.text()).then(plist => {
100
101        let data = parsePlist(plist)
102
103        Object.keys(data.frames).forEach(x => {
104            iconFrames[x] = data.frames[x]
105        })
106
107        let sheetName = iconStr + "-sheet"
108        loadTexture(sheetName, `/iconkit/icons/${iconStr}-uhd.png`).then(texture => {
109            readIconData(texture, data.pos, cb)
110        })
111    })
112}
113
114async function extendTexture(texture, endX, endY) {
115    let img = texture.baseTexture.resource.source
116    let canv = document.createElement('canvas')
117    let ctx = canv.getContext('2d')
118    canv.width = endX
119    canv.height = endY
120    ctx.drawImage(img, 0, 0)
121    let url = canv.toDataURL()
122    let loaded = await PIXI.Assets.load({ src: url, alias: "temp_extended_texture_" + url.slice(-32) + String(Math.random()).slice(2) })
123    return loaded
124}
125
126async function readIconData(texture, data, cb, folder = loadedIcons, warnings = []) {
127    let hasError = false;
128    for (const x of Object.keys(data)) {
129        let bounds = data[x]
130        let size = bounds.rotated ? [bounds.size[1], bounds.size[0]] : bounds.size
131
132        let textureRect = new PIXI.Rectangle(bounds.pos[0], bounds.pos[1], size[0], size[1])
133
134        let endX = bounds.pos[0] + size[0]
135        let endY = bounds.pos[1] + size[1]
136        if (endX > texture.width || endY > texture.height) {
137            if (warnings.length < 1) alert(`Warning: Frame '${x}' expects pixels outside of the width/height of the image file! This is supported by Geometry Dash, but should never happen. ` +
138                `\n\nThe image will be automatically extended, but please consider fixing your sprite offset.\n\nX pos: ${endX} (width: ${texture.width})\nY pos: ${endY} (height: ${texture.height})`)
139            warnings.push(x)
140            if (warnings.length > 99) return
141
142            hasError = true
143            let newTexture = await extendTexture(texture, Math.max(endX, texture.width), Math.max(endY, texture.height))
144            readIconData(newTexture, data, cb, folder, warnings)
145            break;
146        }
147
148        let partTexture = new PIXI.Texture(texture, textureRect)
149        folder[x] = partTexture
150    }
151
152    if (cb && !hasError) {
153        if (warnings.length > 1) alert("The same issue also applies to:\n\n" + warnings.join("\n"))
154        cb(texture, loadedAssets, true)
155    }
156}
157
158let dom_parser = new DOMParser()
159function parsePlist(data) {
160    let plist = dom_parser.parseFromString(data, "text/xml")
161    let frames = plist.children[0].children[0].children[1].children
162    let positionData = {}
163    let dataFrames = {}
164    for (let i = 0; i < frames.length; i += 2) {
165        let frameName = frames[i].innerHTML
166        let frameData = frames[i + 1].children
167        let isRotated = false
168        dataFrames[frameName] = {}
169        positionData[frameName] = {}
170
171        for (let n = 0; n < frameData.length; n += 2) {
172            let keyName = frameData[n].innerHTML
173            let keyData = frameData[n + 1].innerHTML
174            if (["spriteOffset", "spriteSize", "spriteSourceSize"].includes(keyName)) {
175                dataFrames[frameName][keyName] = parseWeirdArray(keyData)
176            }
177
178            else if (keyName == "textureRotated") {
179                isRotated = frameData[n + 1].outerHTML.includes("true")
180                dataFrames[frameName][keyName] = isRotated
181            }
182
183            else if (keyName == "textureRect") {
184                let textureArr = keyData.slice(1, -1).split("},{").map(x => parseWeirdArray(x))
185                positionData[frameName].pos = textureArr[0]
186                positionData[frameName].size = textureArr[1]
187            }
188        }
189
190        if (isRotated) {
191            if (dataFrames[frameName].spriteSize.join(",") == positionData[frameName].size.join(",")) {
192                positionData[frameName].size.reverse()
193            }
194        }
195
196    }
197    return { pos: positionData, frames: dataFrames }
198}
199
200function parseWeirdArray(data) {
201    return data.replace(/[^0-9,-.]/g, "").split(",").map(x => +x)
202}
203
204function padZero(num) {
205    if (isNaN(num)) return num
206    let numStr = num.toString()
207    if (num < 10) numStr = "0" + numStr
208    return numStr
209}
210
211function rgbToDecimal(rgb) {
212    return (rgb.r << 16) + (rgb.g << 8) + rgb.b;
213}
214
215class Icon {
216    constructor(data = {}, cb) {
217        this.app = data.app
218        this.sprite = new PIXI.Container();
219        this.form = data.form || "player"
220        this.id = data.isCustom ? data.id : validateIconID(data.id, this.form)
221        this.colors = data.rawColors || {
222            "1": validNum(data.col1, 0xafafaf),    // primary
223            "2": validNum(data.col2, WHITE),       // secondary
224            "g": validNum(data.colG, validNum(+data.colG, null)), // glow
225            "w": validNum(data.colW, validNum(+data.colW, WHITE)), // white
226            "u": validNum(data.colU, validNum(+data.colU, WHITE)), // ufo
227        }
228
229        this.glow = !!data.glow
230        this.layers = []
231        this.glowLayers = []
232        this.customFiles = null
233        this.complex = ["spider", "robot"].includes(this.form)
234        this.quality = data.quality ? (qualities[data.quality.toLowerCase()] || 'uhd') : 'uhd'
235
236        if (data.isCustom && data.files) this.customFiles = data.files
237
238        // most forms
239        if (!this.complex) {
240            let extraSettings = {}
241            if (data.noUFODome) extraSettings.noDome = true
242            if (data.isCustom) extraSettings.customFiles = this.customFiles
243            if (this.form == "player_ball") {
244                this.ballSpeed = (data.ballSpeed || 0)
245                if (this.ballSpeed != 0 && !this.ballRolling) this.rollBall()
246            }
247            let basicIcon = new IconPart(this.form, this.id, this.colors, this.glow, extraSettings)
248            this.sprite.addChild(basicIcon.sprite)
249            this.layers.push(basicIcon)
250
251            let foundGlow = basicIcon.sections.find(x => x.colorType == "g")
252            if (foundGlow) this.glowLayers.push(foundGlow)
253        }
254
255        // spider + robot
256        else {
257            let idlePosition = this.getAnimation(data.animation, data.animationForm).frames[0]
258            let cFiles = data.isCustom ? this.customFiles : undefined
259            idlePosition.forEach((x, y) => {
260                x.name = iconStuff.robotAnimations.info[this.form].names[y]
261                let part = new IconPart(this.form, this.id, this.colors, false, { part: x, skipGlow: true, customFiles: cFiles })
262                positionPart(x, y, part.sprite, this.form, false, this.quality)
263
264                let glowPart = new IconPart(this.form, this.id, this.colors, true, { part: x, onlyGlow: true, customFiles: cFiles })
265                positionPart(x, y, glowPart.sprite, this.form, true, this.quality)
266                glowPart.sprite.visible = (this.glow || this.colors[1] === 0)
267                this.glowLayers.push(glowPart)
268
269                this.layers.push(part)
270                this.sprite.addChild(part.sprite)
271            })
272
273            let fullGlow = new PIXI.Container();
274            this.glowLayers.forEach(x => fullGlow.addChild(x.sprite))
275            this.sprite.addChildAt(fullGlow, 0)
276            if (typeof Ease !== "undefined") this.ease = new Ease.Ease()
277            this.animationSpeed = Math.abs(Number(data.animationSpeed) || 1)
278            if (data.animation) this.setAnimation(data.animation, data.animationForm)
279        }
280
281        if (this.quality != 'uhd') this.sprite.scale.set(4 / getPositionMultiplier(this.quality))
282
283        if (!data.dontAdd) {
284            this.app.stage.removeChildren()
285            this.app.stage.addChild(this.sprite)
286        }
287
288        if (cb) cb(this)
289
290    }
291
292    updatePosition() {
293        this.sprite.position.set(this.app.renderer.width / 2, (this.app.renderer.height / 2) + (yOffsets[this.form] || 0))
294    }
295
296    getAllLayers() {
297        let allLayers = [];
298        (this.complex ? this.glowLayers : []).concat(this.layers).forEach(x => x.sections.forEach(s => allLayers.push(s)))
299        return allLayers
300    }
301
302    getLayerArr() {
303        if (!this.complex) return this.layers
304        else return this.layers.concat({ sections: this.glowLayers.map(x => x.sections[0]), part: { name: "Glow" } })
305    }
306
307    setColor(colorType, newColor, extra = {}) {
308        let colorStr = String(colorType).toLowerCase()
309        if (!colorType || !Object.keys(this.colors).includes(colorStr)) return
310        else this.colors[colorStr] = newColor
311        let newGlow = getGlowColor(this.colors)
312        this.getAllLayers().forEach(x => {
313            if (colorType != "g" && x.colorType == colorStr) x.setColor(newColor)
314            if (!extra.ignoreGlow && x.colorType == "g") x.setColor(newGlow)
315        })
316        if (!this.glow && colorStr == "1") {
317            let shouldGlow = newColor == 0
318            this.glowLayers.forEach(x => x.sprite.visible = shouldGlow)
319        }
320
321        if (this.secondaryIcon) this.secondaryIcon.setColor(colorType, newColor, extra)
322    }
323
324    copyColorsFrom(i1) {
325        Object.entries(i1.colors).forEach(c => {
326            this.setColor(c[0], c[1])
327        })
328    }
329
330    setGlow(toggle) {
331        this.glow = !!toggle
332        this.glowLayers.forEach(x => x.sprite.visible = (this.colors["1"] == 0 || this.glow))
333
334        if (this.secondaryIcon) this.secondaryIcon.setGlow(toggle)
335    }
336
337    formName() {
338        return formNames[this.form] || this.form
339    }
340
341    isGlowing() {
342        return this.glowLayers[0] && this.glowLayers[0].sprite.visible
343    }
344
345    // icon inside ships, ufos, etc
346    addSecondaryIcon(id = 1, skipLoad, cb) {
347        let offset = cubeOffsets[this.form]
348        if (!offset) return
349
350        let providedIcon = (typeof id == "object" && id.sprite)
351
352        if (!providedIcon && !skipLoad) return loadIconLayers("player", id, () => this.addSecondaryIcon(id, true, cb))
353
354        let i2 = providedIcon ? id : new Icon({
355            app: this.app, form: "player", id, dontAdd: true,
356            rawColors: this.colors, glow: this.glow
357        })
358
359        // remove existing
360        if (this.secondaryIcon) this.removeSecondaryIcon()
361
362        this.secondaryIcon = i2
363
364        let iconScale = getPositionMultiplier(this.quality) / 4
365        
366        i2.sprite.scale.set(offset.scale * iconScale)
367        i2.sprite.position.set(offset.x * 8 * iconScale, offset.y * -8 * iconScale)
368        this.sprite.children[0].addChildAt(i2.sprite, 1)
369
370        if (providedIcon) {
371            i2.copyColorsFrom(this)
372            i2.setGlow(this.glow)
373        }
374
375        if (cb) cb(this)
376    }
377
378    removeSecondaryIcon(destroy) {
379        if (this.secondaryIcon) {
380            if (destroy) this.secondaryIcon.sprite.destroy()
381            else this.sprite.children[0].children = this.sprite.children[0].children.filter(x => x != this.secondaryIcon.sprite)
382            this.secondaryIcon = null
383        }
384    }
385
386    setBallSpeed(speed, stop) {
387        if (this.form != "player_ball") return
388        this.ballSpeed = speed
389
390        if (stop) this.sprite.angle = 0
391        else if (this.ballSpeed != 0 && !this.ballRolling) this.rollBall()
392    }
393
394    rollBall() {
395        if (!this.ballSpeed) return this.ballRolling = false
396        this.ballRolling = true
397        this.sprite.angle += this.ballSpeed
398        requestAnimationFrame(() => this.rollBall())
399    }
400
401    getAnimation(name, animForm) {
402        let animationList = iconStuff.robotAnimations.animations[animForm || this.form]
403        return animationList[name || "idle"] || animationList["idle"]
404    }
405
406    clearAnimation() {
407        this.ease.removeAll()
408        this.isRecording = false
409        this.recordedFrames = []
410        this.animationFrame = 0
411    }
412
413    setAnimation(data, animForm) {
414        let animData = this.getAnimation(data, animForm) || this.getAnimation("idle")
415        this.clearAnimation()
416        this.animationName = data
417        this.runAnimation(animData, data)
418    }
419
420    runAnimation(animData, animName, duration) {
421        animData.frames[this.animationFrame].forEach((newPart, index) => {
422            let section = this.layers[index]
423            let glowSection = this.glowLayers[index]
424            let truePosMultiplier = getPositionMultiplier(this.qual
424ity)
425            if (!section) return
426
427            // gd is weird with negative rotations
428            // this isn't perfect (e.g. robot run2) but it's the best i can do
429            let realRot = newPart.rotation
430            if (realRot < -180) realRot += 360
431
432            let movementData = {
433                x: newPart.pos[0] * truePosMultiplier,
434                y: newPart.pos[1] * truePosMultiplier * -1,
435                scaleX: newPart.scale[0],
436                scaleY: newPart.scale[1],
437                angle: realRot
438            }
439            if (newPart.flipped[0]) movementData.scaleX *= -1
440            if (newPart.flipped[1]) movementData.scaleY *= -1
441
442            let dur = (!duration ? 1 : (animData.info.duration / (this.animationSpeed || 1)))
443            let bothSections = [section, glowSection]
444            bothSections.forEach((x, y) => {
445                let easing = this.ease.add(x.sprite, movementData, { duration: duration || 1, ease: 'linear' })
446                let continueAfterEase = animData.frames.length > 1 && y == 0 && index == 0 && animName == this.animationName
447                if (continueAfterEase) easing.on('complete', () => {
448                    this.animationFrame++
449                    if (this.animationFrame >= animData.frames.length) {
450                        if (animData.info.loop) {
451                            this.animationFrame = 0;
452                        }
453                        else setTimeout(() => {
454                            this.animationFrame = 0;
455                            this.runAnimation(animData, animName, dur);
456                        }, 1000);
457                    }
458                    if (this.animationFrame < animData.frames.length) this.runAnimation(animData, animName, dur)
459                })
460            })
461        })
462    }
463
464    recordAnimation(animName = this.animationName) {
465
466        if (!window.UPNG) appendScript("upng")
467        if (!window.pako) appendScript("pako")
468
469        alert("Animation recording is a work in progress!!! If you found this function then feel free to use it, but looping might be a bit buggy.")
470
471        if (!this.animationFrame || this.animationSpeed < 0.1) return console.info("No animation to record!")
472
473        let w = this.app.view.width
474        let h = this.app.view.height
475
476        // needed because so canvas doesn't autocrop :v
477        let bounds = new PIXI.Graphics();
478        bounds.lineStyle(1, 0xFF0000);
479        bounds.drawRect(0, 0, w - 1, h - 1);
480        bounds.alpha = 0
481        this.app.stage.addChild(bounds);
482
483        const RECORDING_FPS = (this.animationSpeed <= 0.5 ? 30 : 60)
484        const FRAME_DELAY = (1000 / RECORDING_FPS)
485
486        this.setAnimation(animName)
487        this.isRecording = true
488        this.recordedFrames = []
489
490        let firstFrame = -1
491        let passedFrame = false
492
493        let snapFrame = () => {
494            if (firstFrame == -1 && this.animationFrame == 0) return // no idea why this works but removing it breaks things
495
496            let frame = new Uint8Array(this.app.renderer.extract.pixels(this.app.stage)).buffer
497            // console.log([this.animationFrame, firstFrame])
498
499            if (firstFrame == -1) firstFrame = this.animationFrame  // on first frame
500            else if (firstFrame != this.animationFrame) passedFrame = true  // on passing first frame
501            else if (passedFrame) { // on returning to first frame
502                clearInterval(recorder)
503                this.app.stage.removeChild(bounds)
504                this.isRecording = false
505
506                console.info("[animation] Downloading .apng file")
507                let apng = UPNG.encode(this.recordedFrames, w, h, 0, new Array(this.recordedFrames.length).fill(FRAME_DELAY))
508                let apngBlob = new Blob([apng], { type: "image/apng" })
509                downloadFile(apngBlob, this.getDownloadString() + "_" + animName + ".apng")
510
511                return
512            }
513
514            this.recordedFrames.push(frame)
515            console.info(`[animation] Recorded frame ${this.animationFrame}`)
516        }
517
518        let recorder = setInterval(async () => {
519            snapFrame()
520        }, FRAME_DELAY);
521        snapFrame()
522    }
523
524    async getDataURL() {
525        let [imgData, pixels] = await Promise.all([
526            this.app.renderer.extract.image(this.sprite, "image/png", 1),
527            this.app.renderer.extract.pixels(this.sprite)
528        ]);
529
530        await new Promise((res, rej) => {
531            imgData.onload = () => res(imgData);
532            imgData.onerror = (e) => rej(e);
533        })
534
535        let spriteSize = [imgData.width || Math.ceil(this.sprite.width), imgData.height || Math.ceil(this.sprite.height)]
536
537        let xRange = [spriteSize[0], 0]
538        let yRange = [spriteSize[1], 0]
539
540        for (let i = 3; i < pixels.length; i += 4) {
541            let alpha = pixels[i]
542            let realIndex = (i - 3) / 4
543            let pos = [realIndex % spriteSize[0], Math.floor(realIndex / spriteSize[0])]
544
545            if (alpha > 10) { // if pixel is not blank...
546                if (pos[0] < xRange[0]) xRange[0] = pos[0]      // if x pos is < the lowest x pos so far
547                else if (pos[0] > xRange[1]) xRange[1] = pos[0] // if x pos is > the highest x pos so far
548                if (pos[1] < yRange[0]) yRange[0] = pos[1]      // if y pos is < the lowest y pos so far
549                else if (pos[1] > yRange[1]) yRange[1] = pos[1] // if y pos is > the highest y pos so far
550            }
551        }
552
553        xRange[1]++
554        yRange[1]++
555
556        let canv = document.createElement("canvas")
557        let ctx = canv.getContext('2d')
558
559        canv.width = xRange[1] - xRange[0]
560        canv.height = yRange[1] - yRange[0]
561        ctx.drawImage(imgData, -xRange[0], -yRange[0])
562
563        return canv.toDataURL("image/png")
564    }
565
566    getDownloadString() {
567        return `${this.formName()}_${this.id}`
568    }
569
570    async pngExport() {
571        let b64data = await this.getDataURL()
572        let downloader = document.createElement('a');
573        downloader.href = b64data
574        downloader.setAttribute("download", `${this.getDownloadString()}.png`);
575        document.body.appendChild(downloader);
576        downloader.click();
577        document.body.removeChild(downloader);
578    }
579
580    async copyToClipboard() {
581        let b64data = await this.getDataURL()
582        let blob = await fetch(b64data).then(res => res.blob())
583        if (typeof ClipboardItem == "undefined") return alert("Clipboard copying is not supported on this browser! :(")
584        let item = new ClipboardItem({ [blob.type]: blob });
585        navigator.clipboard.write([item]);
586    }
587
588    psdExport() {
589        if (!window.agPsd) return appendScript("ag-psd").then(x => this.psdExport())
590            
591        let glowing = this.isGlowing()
592        this.setGlow(true)
593
594        let psd = { width: this.app.stage.width, height: this.app.stage.height, children: [] }
595        
596        let ic2 = this.secondaryIcon ? this.secondaryIcon.getAllLayers() : []
597        let allLayers = this.getAllLayers().concat(ic2)
598        let renderer = this.app.renderer
599        let complex = this.complex
600
601        function addPSDLayer(layer, parent, sprite) {
602            allLayers.forEach(x => x.sprite.alpha = 0)
603            layer.sprite.alpha = 255
604
605            let layerChild = { name: layer.colorName, canvas: renderer.extract.canvas(sprite) }
606            if (layer.colorType == "g") {
607                if (parent.part) layerChild.name = parent.part.name + " glow"
608                if (!complex && !glowing) layerChild.hidden = true
609            }
610            return layerChild
611        }
612
613        let diffIcons = [this]
614        if (this.secondaryIcon) diffIcons.push(this.secondaryIcon)
615
616        diffIcons.forEach((i, idx) => {
617            i.layers.forEach(x => {
618                let iconType = (formNames[i.form] || i.form)
619
620                let partName = x.part ? x.part.name : (iconType[0].toUpperCase() + iconType.slice(1))
621                let folder = {
622                    name: partName,
623                    children: x.sections.map(layer => addPSDLayer(layer, x, this.sprite)),
624                    opened: true
625                }
626                if (idx == 0) psd.children.push(folder)      // main icon
627                else psd.children[0].children.splice(1, 0, folder)    // secondary icon
628            })
629        })
630
631        if (complex) {
632            let glowFolder = { name: "Glow", children: [], opened: true, hidden: !glowing }
633            glowFolder.children = this.glowLayers.map(x => addPSDLayer(x.sections[0], x, this.sprite))
634            psd.children.unshift(glowFolder)
635        }
636
637        allLayers.forEach(x => x.sprite.alpha = 255)
638        let output = agPsd.writePsd(psd)
639        let blob = new Blob([output]);
640        downloadFile(blob, `${this.getDownloadString()}.psd`)
641        this.setGlow(glowing)
642    }
643
644    destroy() {
645        if (this.ease) this.ease.destroy()
646        this.sprite.destroy()
647    }
648}
649
650class IconPart {
651    constructor(form, id, colors, glow, misc = {}) {
652
653        if (colors[1] === 0 && !misc.skipGlow) glow = true // add glow if p1 is black
654
655        let iconPath = `${form}_${padZero(id)}`
656        let partString = misc.part ? "_" + padZero(misc.part.part) : ""
657
658        let sections = {}
659        if (misc.part) this.part = misc.part
660
661        this.sprite = new PIXI.Container();
662        this.sections = []
663
664        if (!misc.skipGlow) {
665            let glowCol = getGlowColor(colors)
666            let glowPath = `${iconPath}${partString}_glow_001.png`
667
668            if (misc.customFiles) {
669                let foundCustomGlow = misc.customFiles[glowPath]
670                if (!foundCustomGlow) {
671                    alert(`Warning: This icon's glow texture (${glowPath}) could not be found! It will be rendered without a glow layer.`)
672                    glowPath = null
673                }
674            }
675
676            if (glowPath) {
677                sections.glow = new IconLayer(glowPath, glowCol, "g", misc.customFiles)
678                if (!glow) sections.glow.sprite.visible = false
679            }
680
681        }
682
683        if (!misc.onlyGlow) {
684            if (form == "bird") { // ufo top
685                sections.ufo = new IconLayer(`${iconPath}_3_001.png`, colors["u"] || WHITE, "u", misc.customFiles)
686                if (misc.noDome) sections.ufo.sprite.visible = false
687            }
688
689            sections.col1 = new IconLayer(`${iconPath}${partString}_001.png`, colors["1"], "1", misc.customFiles)
690            sections.col2 = new IconLayer(`${iconPath}${partString}_2_001.png`, colors["2"], "2", misc.customFiles)
691
692            let extraPath = `${iconPath}${partString}_extra_001.png`
693            let hasExtra = misc.customFiles ? misc.customFiles[extraPath] : iconFrames[extraPath]
694            if (hasExtra) {
695                sections.white = new IconLayer(extraPath, colors["w"], "w", misc.customFiles)
696            }
697        }
698
699        let layerOrder = ["glow", "ufo", "col2", "col1", "white"].map(x => sections[x]).filter(x => x)
700        layerOrder.forEach(x => {
701            this.sections.push(x)
702            this.sprite.addChild(x.sprite)
703        })
704    }
705}
706
707class IconLayer {
708    constructor(path, color, colorType, customFiles) {
709
710        let loadedTexture = loadedIcons[path]
711
712        if (customFiles) {
713            let customPath = customFiles[path];
714            if (!customPath) throw new Error(`Could not find texture: ${path}`)
715            loadedTexture = customPath.texture
716        }
717
718        let loadedOffsets = iconFrames[path]
719        if (customFiles) loadedOffsets = customFiles[path].frames
720
721        this.offsets = loadedOffsets || { spriteOffset: [0, 0] }
722        this.sprite = new PIXI.Sprite(loadedTexture || PIXI.Texture.EMPTY)
723        this.name = path
724
725        this.colorType = colorType
726        this.colorName = colorNames[colorType]
727        this.setColor(color)
728        this.applyOffset()
729
730        if (this.offsets.textureRotated) {
731            this.sprite.angle = -90
732        }
733        this.angleOffset = this.sprite.angle
734
735        this.sprite.anchor.set(0.5)
736    }
737
738    applyOffset() {
739        this.sprite.position.x = Number(this.offsets.spriteOffset[0] || 0)
740        this.sprite.position.y = Number(this.offsets.spriteOffset[1] || 0) * -1
741    }
742
743    setColor(color) {
744        this.color = validNum(color, WHITE)
745        this.sprite.tint = this.color
746    }
747}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.