GDBrowser/iconkit/icon.js
2022-05-31 17:35:30 -04:00

485 lines
No EOL
18 KiB
JavaScript

const WHITE = 0xffffff
const colorNames = { "1": "Color 1", "2": "Color 2", "g": "Glow", "w": "White", "u": "UFO Dome" }
const formNames = { "player": "icon", "player_ball": "ball", "bird": "ufo", "dart": "wave" }
const loader = PIXI.Loader.shared
const loadedNewIcons = {}
let positionMultiplier = 4
function positionPart(part, partIndex, layer, formName, isNew, isGlow) {
layer.position.x += (part.pos[0] * positionMultiplier * (isNew ? 0.5 : 1))
layer.position.y -= (part.pos[1] * positionMultiplier * (isNew ? 0.5 : 1))
layer.scale.x = part.scale[0]
layer.scale.y = part.scale[1]
if (part.flipped[0]) layer.scale.x *= -1
if (part.flipped[1]) layer.scale.y *= -1
layer.angle = part.rotation
layer.zIndex = part.z
if (!isGlow) {
let tintInfo = iconData.robotAnimations.info[formName].tints
let foundTint = tintInfo[partIndex]
if (foundTint > 0) {
let darkenFilter = new PIXI.filters.ColorMatrixFilter();
darkenFilter.brightness(0)
darkenFilter.alpha = (255 - foundTint) / 255
layer.filters = [darkenFilter]
}
}
}
function validNum(val, defaultVal) {
let colVal = +val
return isNaN(colVal) ? defaultVal : colVal
}
function getGlowColor(colors) {
let glowCol = colors["g"] || (colors[2] === 0 ? colors[1] : colors[2])
if (glowCol === 0) glowCol = WHITE // white glow if both colors are black
return glowCol
}
function validateIconID(id, form) {
let realID = Math.min(iconData.newIconCounts[form], Math.abs(validNum(id, 1)))
if (realID == 0 && !["player", "player_ball"].includes(form)) realID = 1
return realID
}
function parseIconColor(col) {
if (!col) return WHITE
else if (typeof col == "string" && col.length >= 6) return parseInt(col, 16)
let rgb = iconData.colors[col]
return rgb ? rgbToDecimal(rgb) : WHITE;
}
function parseIconForm(form) {
let foundForm = iconData.forms[form]
return foundForm ? foundForm.form : "player"
}
function loadIconLayers(form, id, cb) {
let iconStr = `${form}_${padZero(validateIconID(id, form))}`
let texturesToLoad = Object.keys(iconData.gameSheet).filter(x => x.startsWith(iconStr + "_"))
if (loadedNewIcons[texturesToLoad[0]]) return cb(loader, loader.resources, true)
else if (!texturesToLoad.length) {
if (iconData.newIcons.includes(iconStr)) return loadNewIcon(iconStr, cb)
}
loader.add(texturesToLoad.filter(x => !loader.resources[x]).map(x => ({ name: x, url: `/iconkit/icons/${x}` })))
loader.load(cb) // no params
}
// 2.2 icon spritesheets
function loadNewIcon(iconStr, cb) {
fetch(`/iconkit/newicons/${iconStr}-hd.plist`).then(pl => pl.text()).then(plist => {
let data = parseNewPlist(plist)
let sheetName = iconStr + "-sheet"
loader.add({ name: sheetName, url: `/iconkit/newicons/${iconStr}-hd.png` })
loader.load((l, resources) => {
let texture = resources[sheetName].texture
Object.keys(data).forEach(x => {
let bounds = data[x]
let textureRect = new PIXI.Rectangle(bounds.pos[0], bounds.pos[1], bounds.size[0], bounds.size[1])
let partTexture = new PIXI.Texture(texture, textureRect)
loadedNewIcons[x] = partTexture
})
cb(l, resources, true)
})
})
}
let dom_parser = new DOMParser()
function parseNewPlist(data) {
let plist = dom_parser.parseFromString(data, "text/xml")
let iconFrames = plist.children[0].children[0].children[1].children
let positionData = {}
for (let i=0; i < iconFrames.length; i += 2) {
let frameName = iconFrames[i].innerHTML
let frameData = iconFrames[i + 1].children
let isRotated = false
iconData.gameSheet[frameName] = {}
positionData[frameName] = {}
for (let n=0; n < frameData.length; n += 2) {
let keyName = frameData[n].innerHTML
let keyData = frameData[n + 1].innerHTML
if (["spriteOffset", "spriteSize", "spriteSourceSize"].includes(keyName)) {
iconData.gameSheet[frameName][keyName] = parseWeirdArray(keyData)
}
else if (keyName == "textureRotated") {
isRotated = frameData[n + 1].outerHTML.includes("true")
iconData.gameSheet[frameName][keyName] = isRotated
}
else if (keyName == "textureRect") {
let textureArr = keyData.slice(1, -1).split("},{").map(x => parseWeirdArray(x))
positionData[frameName].pos = textureArr[0]
positionData[frameName].size = textureArr[1]
}
}
if (isRotated) positionData[frameName].size.reverse()
}
return positionData
}
function parseWeirdArray(data) {
return data.replace(/[^0-9,-]/g, "").split(",").map(x => +x)
}
function padZero(num) {
let numStr = num.toString()
if (num < 10) numStr = "0" + numStr
return numStr
}
function rgbToDecimal(rgb) {
return (rgb.r << 16) + (rgb.g << 8) + rgb.b;
}
class Icon {
constructor(data={}, cb) {
this.app = data.app
this.sprite = new PIXI.Container();
this.form = data.form || "player"
this.id = validateIconID(data.id, this.form)
this.new = !!data.new
this.colors = {
"1": validNum(data.col1, 0xafafaf), // primary
"2": validNum(data.col2, WHITE), // secondary
"g": validNum(data.colG, validNum(+data.colg, null)), // glow
"w": validNum(data.colW, validNum(+data.colw, WHITE)), // white
"u": validNum(data.colU, validNum(+data.colu, WHITE)), // ufo
}
this.glow = !!data.glow
this.layers = []
this.glowLayers = []
this.complex = ["spider", "robot"].includes(this.form)
// most forms
if (!this.complex) {
let extraSettings = { new: this.new }
if (data.noUFODome) extraSettings.noDome = true
let basicIcon = new IconPart(this.form, this.id, this.colors, this.glow, extraSettings)
this.sprite.addChild(basicIcon.sprite)
this.layers.push(basicIcon)
this.glowLayers.push(basicIcon.sections.find(x => x.colorType == "g"))
}
// spider + robot
else {
let idlePosition = this.getAnimation(data.animation, data.animationForm).frames[0]
idlePosition.forEach((x, y) => {
x.name = iconData.robotAnimations.info[this.form].names[y]
let part = new IconPart(this.form, this.id, this.colors, false, { part: x, skipGlow: true, new: this.new })
positionPart(x, y, part.sprite, this.form, this.new)
let glowPart = new IconPart(this.form, this.id, this.colors, true, { part: x, onlyGlow: true, new: this.new })
positionPart(x, y, glowPart.sprite, this.form, this.new, true)
glowPart.sprite.visible = this.glow
this.glowLayers.push(glowPart)
this.layers.push(part)
this.sprite.addChild(part.sprite)
})
let fullGlow = new PIXI.Container();
this.glowLayers.forEach(x => fullGlow.addChild(x.sprite))
this.sprite.addChildAt(fullGlow, 0)
if (typeof Ease !== "undefined") this.ease = new Ease.Ease()
this.animationSpeed = Math.abs(Number(data.animationSpeed) || 1)
if (data.animation) this.setAnimation(data.animation, data.animationForm)
}
if (this.new) this.sprite.scale.set(2)
this.app.stage.removeChildren()
this.app.stage.addChild(this.sprite)
if (cb) cb(this)
}
getAllLayers() {
let allLayers = [];
(this.complex ? this.glowLayers : []).concat(this.layers).forEach(x => x.sections.forEach(s => allLayers.push(s)))
return allLayers
}
setColor(colorType, newColor, extra={}) {
let colorStr = String(colorType).toLowerCase()
if (!colorType || !Object.keys(this.colors).includes(colorStr)) return
else this.colors[colorStr] = newColor
let newGlow = getGlowColor(this.colors)
this.getAllLayers().forEach(x => {
if (colorType != "g" && x.colorType == colorStr) x.setColor(newColor)
if (!extra.ignoreGlow && x.colorType == "g") x.setColor(newGlow)
})
if (!this.glow && colorStr == "1") {
let shouldGlow = newColor == 0
this.glowLayers.forEach(x => x.sprite.visible = shouldGlow)
}
}
formName() {
return formNames[this.form] || this.form
}
isGlowing() {
return this.glowLayers[0].sprite.visible
}
setGlow(toggle) {
this.glow = !!toggle
this.glowLayers.forEach(x => x.sprite.visible = (this.colors["1"] == 0 || this.glow))
}
getAnimation(name, animForm) {
let animationList = iconData.robotAnimations.animations[animForm || this.form]
return animationList[name || "idle"] || animationList["idle"]
}
setAnimation(data, animForm) {
let animData = this.getAnimation(data, animForm) || this.getAnimation("idle")
this.ease.removeAll()
this.animationFrame = 0
this.animationName = data
this.runAnimation(animData, data)
}
runAnimation(animData, animName, duration) {
animData.frames[this.animationFrame].forEach((newPart, index) => {
let section = this.layers[index]
let glowSection = this.glowLayers[index]
let truePosMultiplier = this.new ? positionMultiplier * 0.5 : positionMultiplier
if (!section) return
// gd is weird with negative rotations
let realRot = newPart.rotation
if (realRot < -180) realRot += 360
let movementData = {
x: newPart.pos[0] * truePosMultiplier,
y: newPart.pos[1] * truePosMultiplier * -1,
scaleX: newPart.scale[0],
scaleY: newPart.scale[1],
rotation: realRot * (Math.PI / 180) // radians
}
if (newPart.flipped[0]) movementData.scaleX *= -1
if (newPart.flipped[1]) movementData.scaleY *= -1
let bothSections = [section, glowSection]
bothSections.forEach((x, y) => {
let easing = this.ease.add(x.sprite, movementData, { duration: duration || 1, ease: 'linear' })
let continueAfterEase = animData.frames.length > 1 && y == 0 && index == 0 && animName == this.animationName
if (continueAfterEase) easing.on('complete', () => {
this.animationFrame++
if (this.animationFrame >= animData.frames.length) {
if (animData.info.loop) this.animationFrame = 0
}
if (this.animationFrame < animData.frames.length) this.runAnimation(animData, animName, !duration ? 1 : (animData.info.duration / (this.animationSpeed || 1)))
})
})
})
}
autocrop() {
// find actual icon size by reading pixel data (otherwise there's whitespace and shit)
if (this.new) this.sprite.scale.set(1)
let spriteSize = [Math.round(this.sprite.width), Math.round(this.sprite.height)]
let pixels = this.app.renderer.plugins.extract.pixels(this.sprite);
let xRange = [spriteSize[0], 0]
let yRange = [spriteSize[1], 0]
this.preCrop = { pos: [this.sprite.position.x, this.sprite.position.y], canvas: [this.app.renderer.width, this.app.renderer.height] }
for (let i=3; i < pixels.length; i += 4) {
let alpha = pixels[i]
let realIndex = (i-3) / 4
let pos = [realIndex % spriteSize[0], Math.floor(realIndex / spriteSize[0])]
if (alpha > 10) { // if pixel is not blank...
if (pos[0] < xRange[0]) xRange[0] = pos[0] // if x pos is < the lowest x pos so far
else if (pos[0] > xRange[1]) xRange[1] = pos[0] // if x pos is > the highest x pos so far
if (pos[1] < yRange[0]) yRange[0] = pos[1] // if y pos is < the lowest y pos so far
else if (pos[1] > yRange[1]) yRange[1] = pos[1] // if y pos is > the highest y pos so far
}
}
// this took hours to figure out. i fucking hate my life
xRange[1]++
yRange[1]++
let realWidth = xRange[1] - xRange[0]
let realHeight = yRange[1] - yRange[0]
this.app.renderer.resize(realWidth, realHeight)
let bounds = this.sprite.getBounds()
this.sprite.position.x -= bounds.x
this.sprite.position.y -= bounds.y
this.sprite.position.x += (spriteSize[0] - xRange[1]) - xRange[0]
}
revertCrop() {
this.app.renderer.resize(...this.preCrop.canvas)
this.sprite.position.set(...this.preCrop.pos)
if (this.new) this.sprite.scale.set(2)
}
toDataURL(dataType="image/png") {
this.autocrop()
this.app.renderer.render(this.app.stage);
let b64data = this.app.view.toDataURL(dataType);
this.revertCrop()
return b64data
}
pngExport() {
let b64data = this.toDataURL()
let downloader = document.createElement('a');
downloader.href = b64data
downloader.setAttribute("download", `${this.formName()}_${this.id}.png`);
document.body.appendChild(downloader);
downloader.click();
document.body.removeChild(downloader);
}
copyToClipboard() {
this.autocrop()
this.app.renderer.render(app.stage);
this.app.view.toBlob(blob => {
let item = new ClipboardItem({ "image/png": blob });
navigator.clipboard.write([item]);
});
this.revertCrop()
}
psdExport() {
if (typeof agPsd === "undefined") throw new Error("ag-psd not imported!")
let glowing = this.isGlowing()
this.setGlow(true)
let psd = { width: this.app.stage.width, height: this.app.stage.height, children: [] }
let allLayers = this.getAllLayers()
let renderer = this.app.renderer
let complex = this.complex
function addPSDLayer(layer, parent, sprite) {
allLayers.forEach(x => x.sprite.alpha = 0)
layer.sprite.alpha = 255
let layerChild = { name: layer.colorName, canvas: renderer.plugins.extract.canvas(sprite) }
if (layer.colorType == "g") {
if (parent.part) layerChild.name = parent.part.name + " glow"
else layerChild.blendMode = "linear dodge"
if (!complex && !glowing) layerChild.hidden = true
}
return layerChild
}
this.layers.forEach(x => {
let partName = x.part ? x.part.name : "Icon"
let folder = {
name: partName,
children: x.sections.map(layer => addPSDLayer(layer, x, this.sprite)),
opened: true
}
psd.children.push(folder)
})
if (complex) {
let glowFolder = { name: "Glow", children: [], opened: true, hidden: !glowing }
glowFolder.children = this.glowLayers.map(x => addPSDLayer(x.sections[0], x, this.sprite))
psd.children.unshift(glowFolder)
}
allLayers.forEach(x => x.sprite.alpha = 255)
let output = agPsd.writePsd(psd)
let blob = new Blob([output]);
let downloader = document.createElement('a');
downloader.href = URL.createObjectURL(blob);
downloader.setAttribute("download", `${this.formName()}_${this.id}.psd`);
document.body.appendChild(downloader);
downloader.click();
document.body.removeChild(downloader);
this.setGlow(glowing)
}
}
class IconPart {
constructor(form, id, colors, glow, misc={}) {
if (colors[1] == 0 && !misc.skipGlow) glow = true // add glow if p1 is black
let iconPath = `${form}_${padZero(id)}`
let partString = misc.part ? "_" + padZero(misc.part.part) : ""
let sections = {}
if (misc.part) this.part = misc.part
this.sprite = new PIXI.Container();
this.sections = []
if (!misc.skipGlow) {
let glowCol = getGlowColor(colors)
sections.glow = new IconLayer(`${iconPath}${partString}_glow_001.png`, glowCol, "g", misc.new)
if (!glow) sections.glow.sprite.visible = false
}
if (!misc.onlyGlow) {
if (form == "bird" && !misc.noDome) { // ufo top
sections.ufo = new IconLayer(`${iconPath}_3_001.png`, WHITE, "u", misc.new)
}
sections.col1 = new IconLayer(`${iconPath}${partString}_001.png`, colors["1"], "1", misc.new)
sections.col2 = new IconLayer(`${iconPath}${partString}_2_001.png`, colors["2"], "2", misc.new)
let extraPath = `${iconPath}${partString}_extra_001.png`
if (iconData.gameSheet[extraPath]) {
sections.white = new IconLayer(extraPath, colors["w"], "w", misc.new)
}
}
let layerOrder = ["glow", "ufo", "col2", "col1", "white"].map(x => sections[x]).filter(x => x)
layerOrder.forEach(x => {
this.sections.push(x)
this.sprite.addChild(x.sprite)
})
}
}
class IconLayer {
constructor(path, color, colorType, isNew) {
let loadedTexture = isNew ? loadedNewIcons[path] : loader.resources[path]
this.offsets = iconData.gameSheet[path] || { spriteOffset: [0, 0] }
this.sprite = new PIXI.Sprite(loadedTexture ? isNew ? loadedTexture : loadedTexture.texture : PIXI.Texture.EMPTY)
this.colorType = colorType
this.colorName = colorNames[colorType]
this.setColor(color)
this.sprite.position.x += this.offsets.spriteOffset[0]
this.sprite.position.y -= this.offsets.spriteOffset[1]
if (this.offsets.textureRotated) {
this.sprite.angle = -90
}
this.angleOffset = this.sprite.angle
this.sprite.anchor.set(0.5)
}
setColor(color) {
this.color = validNum(color, WHITE)
this.sprite.tint = this.color
}
}