diff --git a/assets/mods/basegame/locales/en/game.json b/assets/mods/basegame/locales/en/game.json index 9d566dda6..1e9b6c61c 100644 --- a/assets/mods/basegame/locales/en/game.json +++ b/assets/mods/basegame/locales/en/game.json @@ -16,5 +16,7 @@ "GAME_ACTION_QUICKSEL": "Quick Select", "GAME_ACTION_CRAFT": "Craft", "GAME_CRAFTING": "Crafting", - "GAME_CRAFTABLE_ITEMS": "Craftable Items" + "GAME_CRAFTABLE_ITEMS": "Craftable Items", + "CONTEXT_WORLD_SEARCH": "World Search", + "CONTEXT_WORLD_LIST": "Worlds List" } \ No newline at end of file diff --git a/src/net/torvald/terrarum/UIItemInventoryCatBar.kt b/src/net/torvald/terrarum/UIItemInventoryCatBar.kt index 8b181e061..424ea08a5 100644 --- a/src/net/torvald/terrarum/UIItemInventoryCatBar.kt +++ b/src/net/torvald/terrarum/UIItemInventoryCatBar.kt @@ -136,11 +136,11 @@ class UIItemInventoryCatBar( private val underlineColour = Color(0xeaeaea_40.toInt()) private val underlineHighlightColour = mainButtons[0].highlightCol - private var highlighterXPos = mainButtons[selectedIndex].posX.toFloat() + private var highlighterXPos = mainButtons[selectedIndex].posX private var highlighterXStart = highlighterXPos private var highlighterXEnd = highlighterXPos - private val highlighterYPos = catIcons.tileH + 4f + private val highlighterYPos = catIcons.tileH + 4 private var highlighterMoving = false private val highlighterMoveDuration: Second = 0.15f private var highlighterMoveTimer: Second = 0f @@ -196,11 +196,11 @@ class UIItemInventoryCatBar( highlighterMoveTimer += delta highlighterXPos = Movement.moveQuick( - highlighterXStart, - highlighterXEnd, + highlighterXStart.toFloat(), + highlighterXEnd.toFloat(), highlighterMoveTimer, highlighterMoveDuration - ) + ).roundToInt() if (highlighterMoveTimer > highlighterMoveDuration) { highlighterMoveTimer = 0f @@ -224,10 +224,10 @@ class UIItemInventoryCatBar( // normal stuffs val oldIndex = selectedIndex - highlighterXStart = mainButtons[selectedIndex].posX.toFloat() // using old selectedIndex + highlighterXStart = mainButtons[selectedIndex].posX // using old selectedIndex selectedIndex = index highlighterMoving = true - highlighterXEnd = mainButtons[selectedIndex].posX.toFloat() // using new selectedIndex + highlighterXEnd = mainButtons[selectedIndex].posX // using new selectedIndex selectionChangeListener?.invoke(oldIndex, index) } @@ -290,12 +290,12 @@ class UIItemInventoryCatBar( if (selectedPanel == 1) { // indicator batch.color = underlineHighlightColour - batch.draw(underlineIndTex, (highlighterXPos - buttonGapSize / 2).round(), posY + highlighterYPos) + batch.draw(underlineIndTex, (highlighterXPos - buttonGapSize / 2), posY + highlighterYPos.toFloat()) // label batch.color = Color.WHITE catIconsLabels[selectedIcon]().let { - App.fontGame.draw(batch, it, posX + ((width - App.fontGame.getWidth(it)) / 2).toFloat(), posY + highlighterYPos + 4) + App.fontGame.draw(batch, it, posX + ((width - App.fontGame.getWidth(it)) / 2), posY + highlighterYPos + 4) } } diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIInventoryFull.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIInventoryFull.kt index 43ac11b70..bcff5f906 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIInventoryFull.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIInventoryFull.kt @@ -40,8 +40,12 @@ class UIInventoryFull( const val REQUIRED_MARGIN: Int = 138 // hard-coded value. Don't know the details. Range: [91-146]. I chose MAX-8 because cell gap is 8 const val CELLS_HOR = 10 - val CELLS_VRT: Int; get() = (App.scr.height - REQUIRED_MARGIN - 134 + UIItemInventoryItemGrid.listGap) / // 134 is another magic number - (UIItemInventoryElemSimple.height + UIItemInventoryItemGrid.listGap) + + fun getCellCountVertically(cellHeight: Int, gapHeight: Int = UIItemInventoryItemGrid.listGap): Int { + return (App.scr.height - REQUIRED_MARGIN - 134 + gapHeight) / // 134 is another magic number + (cellHeight + gapHeight) + } + val CELLS_VRT: Int; get() = getCellCountVertically(UIItemInventoryElemSimple.height, UIItemInventoryItemGrid.listGap) const val itemListToEquipViewGap = UIItemInventoryItemGrid.listGap // used to be 24; figured out that the extra gap does nothig diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt b/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt index 70eab78ed..4f40e08f1 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt @@ -532,7 +532,7 @@ class UIItemPlayerCells( private val litCol = Toolkit.Theme.COL_MOUSE_UP private val cellCol = CELL_COL - private val defaultCol = Color.WHITE + private val defaultCol = Toolkit.Theme.COL_LIST_DEFAULT private val hruleCol = Color(1f,1f,1f,0.35f) private val hruleColLit = litCol.cpy().sub(0f,0f,0f,0.65f) @@ -734,7 +734,7 @@ class UIItemWorldCells( private val colourBad = Color(0xFF0011FF.toInt()) private val cellCol = CELL_COL - private var highlightCol: Color = Color.WHITE + private var highlightCol: Color = Toolkit.Theme.COL_LIST_DEFAULT override var clickOnceListener: ((Int, Int, Int) -> Unit)? = { _: Int, _: Int, _: Int -> UILoadGovernor.worldDisk = skimmer @@ -746,7 +746,7 @@ class UIItemWorldCells( override fun update(delta: Float) { super.update(delta) - highlightCol = if (mouseUp) Toolkit.Theme.COL_MOUSE_UP else Color.WHITE + highlightCol = if (mouseUp) Toolkit.Theme.COL_MOUSE_UP else Toolkit.Theme.COL_LIST_DEFAULT } override fun render(batch: SpriteBatch, camera: Camera) { diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UINewWorld.kt b/src/net/torvald/terrarum/modulebasegame/ui/UINewWorld.kt index c74a312ac..2941703f5 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UINewWorld.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UINewWorld.kt @@ -86,8 +86,6 @@ class UINewWorld(val remoCon: UIRemoCon) : UICanvas() { private val goButton = UIItemTextButton(this, "MENU_LABEL_CONFIRM_BUTTON", drawX + width/2 + (width/2 - goButtonWidth) / 2, drawY + height - 24, goButtonWidth, true, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true) init { - tex.forEach { it.flip(false, false) } - goButton.touchDownListener = { _, _, _, _ -> // printdbg(this, "generate! Size=${sizeSelector.selection}, Name=${nameInput.getTextOrPlaceholder()}, Seed=${seedInput.getTextOrPlaceholder()}") diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortal.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortal.kt index e94651730..0ff6467a7 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortal.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortal.kt @@ -4,9 +4,15 @@ import com.badlogic.gdx.graphics.Camera import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.g2d.SpriteBatch import net.torvald.terrarum.* -import net.torvald.terrarum.ui.Toolkit -import net.torvald.terrarum.ui.UICanvas -import net.torvald.terrarum.ui.UIItemHorizontalFadeSlide +import net.torvald.terrarum.langpack.Lang +import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.INVENTORY_CELLS_OFFSET_Y +import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.YPOS_CORRECTION +import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.drawBackground +import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.internalHeight +import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.internalWidth +import net.torvald.terrarum.ui.* +import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack +import net.torvald.unicode.getKeycapPC /** * Structure: @@ -23,8 +29,8 @@ class UIWorldPortal : UICanvas( toggleButtonLiteral = App.getConfigInt("control_gamepad_start"), ) { - override var width = App.scr.width - override var height = App.scr.height + override var width: Int = Toolkit.drawWidth + override var height: Int = App.scr.height @@ -41,23 +47,30 @@ class UIWorldPortal : UICanvas( fun requestTransition(target: Int) = transitionPanel.requestTransition(target) - val catBar = UIItemInventoryCatBar( + val catBar = UIItemWorldPortalTopBar( this, - (width - UIInventoryFull.catBarWidth) / 2, - 42 - UIInventoryFull.YPOS_CORRECTION + (App.scr.height - UIInventoryFull.internalHeight) / 2, - UIInventoryFull.internalWidth, - UIInventoryFull.catBarWidth, - true + 0, + 42 - YPOS_CORRECTION + (App.scr.height - internalHeight) / 2, ) { i -> if (!panelTransitionLocked) requestTransition(i) } + private val SP = "\u3000 " + val portalListingControlHelp: String + get() = if (App.environment == RunningEnvironment.PC) + "${getKeycapPC(App.getConfigInt("control_key_inventory"))} ${Lang["GAME_ACTION_CLOSE"]}" + else + "${App.gamepadLabelStart} ${Lang["GAME_ACTION_CLOSE"]}" + + "$SP${App.gamepadLabelLT} ${Lang["GAME_WORLD_SEARCH"]}" + + "$SP${App.gamepadLabelRT} ${Lang["GAME_INVENTORY"]}" + + private val transitionalSearch = UIWorldPortalSearch(this) private val transitionalListing = UIWorldPortalListing(this) private val transitionalCargo = UIWorldPortalCargo(this) private val transitionPanel = UIItemHorizontalFadeSlide( this, - (width - UIInventoryFull.internalWidth) / 2, - UIInventoryFull.INVENTORY_CELLS_OFFSET_Y(), + (width - internalWidth) / 2, + INVENTORY_CELLS_OFFSET_Y(), width, App.scr.height, 1f, @@ -72,7 +85,10 @@ class UIWorldPortal : UICanvas( } - + internal var xEnd = (width + internalWidth).div(2).toFloat() + private set + internal var yEnd = -YPOS_CORRECTION + (App.scr.height + internalHeight).div(2).toFloat() + private set @@ -82,7 +98,7 @@ class UIWorldPortal : UICanvas( } override fun renderUI(batch: SpriteBatch, camera: Camera) { - UIInventoryFull.drawBackground(batch, handler.opacity) + drawBackground(batch, handler.opacity) // UI items catBar.render(batch, camera) @@ -123,4 +139,83 @@ class UIWorldPortal : UICanvas( INGAME.setTooltipMessage(null) // required! } +} + +class UIItemWorldPortalTopBar( + parentUI: UIWorldPortal, + initialX: Int, + initialY: Int, + val panelTransitionReqFun: (Int) -> Unit = {} // for side buttons; for the selection change, override selectionChangeListener +) : UIItem(parentUI, initialX, initialY) { + + override val width = 580 + override val height = 25 + + init { + CommonResourcePool.addToLoadingList("terrarum-basegame-worldportalicons") { + TextureRegionPack(ModMgr.getGdxFile("basegame", "gui/worldportal_catbar.tga"), 20, 20) + } + CommonResourcePool.loadAll() + } + + private val genericIcons: TextureRegionPack = CommonResourcePool.getAsTextureRegionPack("inventory_category") + private val icons = CommonResourcePool.getAsTextureRegionPack("terrarum-basegame-worldportalicons") + private val catIconImages = listOf( + icons.get(0, 0), + genericIcons.get(16,0), + icons.get(1, 0), + genericIcons.get(17,0), + icons.get(2, 0), + ) + private val catIconLabels = listOf( + "CONTEXT_WORLD_SEARCH", + "", + "CONTEXT_WORLD_LIST", + "GAME_INVENTORY", + "", + ) + private val buttonGapSize = 120 + private val highlighterYPos = icons.tileH + 4 + + var selection = 2 + + private val buttons = Array(5) { + val xoff = if (it == 1) -32 else if (it == 3) 32 else 0 + UIItemImageButton( + parentUI, + catIconImages[it], + activeBackCol = Color(0), + backgroundCol = Color(0), + highlightBackCol = Color(0), + activeBackBlendMode = BlendMode.NORMAL, + initialX = (Toolkit.drawWidth - width) / 2 + it * (buttonGapSize + 20) + xoff, + initialY = posY, + inactiveCol = if (it % 2 == 0) Color.WHITE else Color(0xffffff7f.toInt()), + activeCol = if (it % 2 == 0) Toolkit.Theme.COL_MOUSE_UP else Color(0xffffff7f.toInt()), + highlightable = (it % 2 == 0) + ) + } + + override fun render(batch: SpriteBatch, camera: Camera) { + super.render(batch, camera) + + // button + buttons.forEach { it.render(batch, camera) } + + // label + batch.color = Color.WHITE + val text = Lang[catIconLabels[selection]] + App.fontGame.draw(batch, text, buttons[selection].posX + 10 - (App.fontGame.getWidth(text) / 2), posY + highlighterYPos + 4) + + + blendNormalStraightAlpha(batch) + + + + } + + override fun dispose() { + } + + } \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalListing.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalListing.kt index c4e5c3456..2c0eade59 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalListing.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalListing.kt @@ -2,19 +2,18 @@ package net.torvald.terrarum.modulebasegame.ui import com.badlogic.gdx.graphics.Camera import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.SpriteBatch -import com.badlogic.gdx.graphics.g2d.TextureRegion -import com.badlogic.gdx.utils.Json +import com.badlogic.gdx.utils.GdxRuntimeException import net.torvald.terrarum.* import net.torvald.terrarum.gameactors.AVKey import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.INVENTORY_CELLS_OFFSET_Y -import net.torvald.terrarum.modulebasegame.ui.UIItemInventoryItemGrid.Companion.listGap +import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.getCellCountVertically import net.torvald.terrarum.realestate.LandUtil.CHUNK_H import net.torvald.terrarum.realestate.LandUtil.CHUNK_W import net.torvald.terrarum.savegame.ByteArray64Reader import net.torvald.terrarum.savegame.DiskSkimmer import net.torvald.terrarum.savegame.EntryFile +import net.torvald.terrarum.serialise.Common import net.torvald.terrarum.serialise.ascii85toUUID import net.torvald.terrarum.ui.Toolkit import net.torvald.terrarum.ui.UICanvas @@ -22,8 +21,8 @@ import net.torvald.terrarum.ui.UIItem import net.torvald.terrarum.ui.UIItemTextButton import net.torvald.terrarum.utils.JsonFetcher import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack +import net.torvald.unicode.EMDASH import java.util.* -import kotlin.math.roundToInt /** * Created by minjaesong on 2023-05-19. @@ -34,27 +33,19 @@ class UIWorldPortalListing(val full: UIWorldPortal) : UICanvas() { override var width: Int = Toolkit.drawWidth override var height: Int = App.scr.height - private val cellHeight = 48 private val buttonHeight = 24 - private val gridGap = listGap + private val gridGap = 10 private val worldList = ArrayList() - private var selectedWorld: DiskSkimmer? = null - - - private val cellCol = UIInventoryFull.CELL_COL - private var highlightCol: Color = Color.WHITE - - private val thumbw = 360 private val thumbh = 240 private val hx = Toolkit.drawWidth.div(2) - private val y = INVENTORY_CELLS_OFFSET_Y() + private val y = INVENTORY_CELLS_OFFSET_Y() + 1 - private val listCount = UIInventoryFull.CELLS_VRT - private val listHeight = cellHeight * listCount + gridGap * (listCount - 1) + private val listCount = getCellCountVertically(UIItemWorldCellsSimple.height, gridGap) + private val listHeight = UIItemWorldCellsSimple.height + (listCount - 1) * (UIItemWorldCellsSimple.height + gridGap) private val memoryGaugeWidth = 360 private val deleteButtonWidth = (memoryGaugeWidth - gridGap) / 2 @@ -80,7 +71,8 @@ class UIWorldPortalListing(val full: UIWorldPortal) : UICanvas() { data class WorldInfo( val uuid: UUID, val diskSkimmer: DiskSkimmer, - val dimensionInChunks: Int + val dimensionInChunks: Int, + val seed: Long ) init { @@ -98,30 +90,49 @@ class UIWorldPortalListing(val full: UIWorldPortal) : UICanvas() { private var chunksUsed = 0 private val chunksMax = 100000 + private lateinit var worldCells: Array + override fun show() { worldList.clear() worldList.addAll((INGAME.actorGamer.actorValue.getAsString(AVKey.WORLD_PORTAL_DICT) ?: "").split(",").filter { it.isNotBlank() }.map { it.ascii85toUUID().let { it to App.savegameWorlds[it] } }.filter { it.second != null }.map { (uuid, disk) -> - chunksUsed = worldList.sumOf { + var chunksCount = 0 + var seed = 0L + worldList.forEach { var w = 0 var h = 0 - JsonFetcher.readFromJsonString(ByteArray64Reader((disk!!.requestFile(-1)!!.contents.getContent() as EntryFile).bytes, Charsets.UTF_8)).let { + JsonFetcher.readFromJsonString(ByteArray64Reader((disk!!.requestFile(-1)!!.contents.getContent() as EntryFile).bytes, Common.CHARSET)).let { JsonFetcher.forEachSiblings(it) { name, value -> if (name == "width") w = value.asInt() if (name == "height") h = value.asInt() + if (name == "generatorSeed") seed = value.asLong() } } - (w / CHUNK_W) * (h / CHUNK_H) + chunksCount = (w / CHUNK_W) * (h / CHUNK_H) } - WorldInfo(uuid, disk!!, chunksUsed) + WorldInfo(uuid, disk!!, chunksCount, seed) } as List) chunksUsed = worldList.sumOf { it.dimensionInChunks } + + worldCells = Array(maxOf(worldList.size, listCount)) { + UIItemWorldCellsSimple( + this, + hx + gridGap / 2, + y + (gridGap + UIItemWorldCellsSimple.height) * it, + worldList.getOrNull(it), + worldList.getOrNull(it)?.diskSkimmer?.getDiskName(Common.CHARSET) + ) + } + + uiItems.forEach { it.show() } + worldCells.forEach { it.show() } } override fun updateUI(delta: Float) { - + uiItems.forEach { it.update(delta) } + worldCells.forEach { it.update(delta) } } @@ -132,13 +143,13 @@ class UIWorldPortalListing(val full: UIWorldPortal) : UICanvas() { // draw background // // screencap panel - batch.color = cellCol + batch.color = UIInventoryFull.CELL_COL Toolkit.fillArea(batch, hx - thumbw - gridGap/2, y, thumbw, thumbh) // draw border // // screencap panel - batch.color = highlightCol + batch.color = Toolkit.Theme.COL_LIST_DEFAULT Toolkit.drawBoxBorder(batch, hx - thumbw - gridGap/2 - 1, y - 1, thumbw + 2, thumbh + 2) @@ -156,27 +167,81 @@ class UIWorldPortalListing(val full: UIWorldPortal) : UICanvas() { uiItems.forEach { it.render(batch, camera) } + worldCells.forEach { it.render(batch, camera) } + // control hints + batch.color = Color.WHITE + App.fontGame.draw(batch, full.portalListingControlHelp, (hx - thumbw - gridGap/2).toInt(), (full.yEnd - 20).toInt()) + } + + override fun hide() { + uiItems.forEach { it.hide() } + worldCells.forEach { it.hide() } + + worldCells.forEach { try { it.dispose() } catch (_: GdxRuntimeException) {} } } override fun dispose() { - + uiItems.forEach { it.dispose() } + worldCells.forEach { try { it.dispose() } catch (_: GdxRuntimeException) {} } } } class UIItemWorldCellsSimple( - parent: UILoadDemoSavefiles, + parent: UIWorldPortalListing, initialX: Int, initialY: Int, - val skimmer: DiskSkimmer + internal val worldInfo: UIWorldPortalListing.WorldInfo? = null, + internal val worldName: String? = null, ) : UIItem(parent, initialX, initialY) { + companion object { + const val width = 360 + const val height = 46 + } + override val width: Int = 360 override val height: Int = 46 - private val cellCol = UIInventoryFull.CELL_COL - private var highlightCol: Color = Color.WHITE + private val icons = CommonResourcePool.getAsTextureRegionPack("terrarum-basegame-worldportalicons") + + + override fun show() { + super.show() + } + + override fun hide() { + super.hide() + } + + override fun update(delta: Float) { + super.update(delta) + } + + override fun render(batch: SpriteBatch, camera: Camera) { + super.render(batch, camera) + + + // draw background + batch.color = UIInventoryFull.CELL_COL + Toolkit.fillArea(batch, posX, posY, width, height) + + // draw border + val bcol = if (mouseUp && mousePushed) Toolkit.Theme.COL_SELECTED + else if (mouseUp) Toolkit.Theme.COL_MOUSE_UP else Toolkit.Theme.COL_LIST_DEFAULT + batch.color = bcol + Toolkit.drawBoxBorder(batch, posX - 1, posY - 1, width + 2, height + 2) + // draw texts + batch.draw(icons.get(0, 1), posX + 4f, posY + 1f) + App.fontGame.draw(batch, worldName ?: "$EMDASH", posX + 32, posY + 1) + batch.draw(icons.get(1, 1), posX + 4f, posY + 25f) + App.fontGame.draw(batch, if (worldInfo?.seed == null) "$EMDASH" else "${(if (worldInfo.seed > 0) " + " else "")}${worldInfo.seed}" , posX + 32, posY + 25) + // text separator + batch.color = bcol.cpy().sub(0f,0f,0f,0.65f) + Toolkit.fillArea(batch, posX + 2, posY + 23, width - 4, 1) + + } override fun dispose() { } diff --git a/src/net/torvald/terrarum/ui/Toolkit.kt b/src/net/torvald/terrarum/ui/Toolkit.kt index 6f80e9d58..c4d3616a4 100644 --- a/src/net/torvald/terrarum/ui/Toolkit.kt +++ b/src/net/torvald/terrarum/ui/Toolkit.kt @@ -22,7 +22,7 @@ object Toolkit : Disposable { val COL_INVENTORY_CELL_BORDER = Color(1f, 1f, 1f, 0.25f) val COL_CELL_FILL = Color(0x282828C8) - val COL_LIST_DEFAULT = Color.WHITE + val COL_LIST_DEFAULT = Color.WHITE // white val COL_INACTIVE = Color.LIGHT_GRAY val COL_SELECTED = Color(0x00f8ff_ff) // cyan, HIGHLY SATURATED val COL_MOUSE_UP = Color(0xfff066_ff.toInt()) // yellow (all yellows are of low saturation according to the colour science)