-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathRandomnessIcons.kt
359 lines (301 loc) · 11.1 KB
/
RandomnessIcons.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
package com.fwdekker.randomness
import com.intellij.openapi.util.IconLoader
import com.intellij.ui.ColorUtil
import com.intellij.util.IconUtil
import java.awt.Color
import java.awt.Component
import java.awt.Graphics
import java.awt.image.RGBImageFilter
import javax.swing.Icon
import kotlin.math.atan2
/**
* Basic Randomness icons.
*/
object RandomnessIcons {
/**
* The main icon of Randomness.
*/
val RANDOMNESS = IconLoader.findIcon("/icons/randomness.svg")!!
/**
* The template icon for template icons.
*/
val TEMPLATE = IconLoader.findIcon("/icons/template.svg")!!
/**
* The template icon for scheme icons.
*/
val SCHEME = IconLoader.findIcon("/icons/scheme.svg")!!
/**
* An icon for settings.
*/
val SETTINGS = IconLoader.findIcon("/icons/settings.svg")!!
/**
* A filled-in version of [SETTINGS].
*/
val SETTINGS_FILLED = IconLoader.findIcon("/icons/settings-filled.svg")!!
/**
* An icon for arrays.
*/
val ARRAY = IconLoader.findIcon("/icons/array.svg")!!
/**
* A filled-in version of [ARRAY].
*/
val ARRAY_FILLED = IconLoader.findIcon("/icons/array-filled.svg")!!
/**
* An icon for references.
*/
val REFERENCE = IconLoader.findIcon("/icons/reference.svg")!!
/**
* A filled-in version of [REFERENCE].
*/
val REFERENCE_FILLED = IconLoader.findIcon("/icons/reference-filled.svg")!!
/**
* An icon for repeated insertions.
*/
val REPEAT = IconLoader.findIcon("/icons/repeat.svg")!!
/**
* A filled-in version of [REPEAT].
*/
val REPEAT_FILLED = IconLoader.findIcon("/icons/repeat-filled.svg")!!
}
/**
* A colored icon with some text in it.
*
* @property base The underlying icon which should be given color; must be square.
* @property text The text to display inside the [base].
* @property colors The colors to give to the [base].
*/
data class TypeIcon(val base: Icon, val text: String, val colors: List<Color>) : Icon {
init {
require(colors.isNotEmpty()) { "At least one color must be defined." }
}
/**
* Returns an icon that describes both this icon's type and [other]'s type.
*
* @param other the icon to combine this icon with
* @return an icon that describes both this icon's type and [other]'s type
*/
fun combineWith(other: TypeIcon) =
TypeIcon(
RandomnessIcons.TEMPLATE,
if (this.text == other.text) this.text else "",
this.colors + other.colors
)
/**
* Paints the colored text icon.
*
* @param c a [Component] to get properties useful for painting
* @param g the graphics context
* @param x the X coordinate of the icon's top-left corner
* @param y the Y coordinate of the icon's top-left corner
*/
override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) {
if (c == null || g == null) return
val filter = RadialColorReplacementFilter(colors, Pair(iconWidth / 2, iconHeight / 2))
IconUtil.filterIcon(base, { filter }, c).paintIcon(c, g, x, y)
val textIcon = IconUtil.textToIcon(text, c, FONT_SIZE * iconWidth)
textIcon.paintIcon(c, g, x + (iconWidth - textIcon.iconWidth) / 2, y + (iconHeight - textIcon.iconHeight) / 2)
}
/**
* The width of the base icon.
*/
override fun getIconWidth() = base.iconWidth
/**
* The height of the base icon.
*/
override fun getIconHeight() = base.iconHeight
/**
* Holds constants.
*/
companion object {
/**
* The scale of the text inside the icon relative to the icon's size.
*/
const val FONT_SIZE = 12f / 32f
}
}
/**
* An overlay icon, which can be displayed on top of other icons.
*
* This icon is drawn as the [base] surrounded by a small margin of background color, which creates visual distance
* between the overlay and the rest of the icon this overlay is shown in top of. The background color is determined when
* the icon is drawn.
*
* @property base The base of the icon; must be square.
* @property background The background shape to ensure that the small margin of background color is also applied inside
* the [base], or `null` if [base] is already a solid shape.
*/
data class OverlayIcon(val base: Icon, val background: Icon? = null) : Icon {
/**
* Paints the overlay icon.
*
* @param c a [Component] to get properties useful for painting
* @param g the graphics context
* @param x the X coordinate of the icon's top-left corner
* @param y the Y coordinate of the icon's top-left corner
*/
override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) {
if (c == null || g == null) return
IconUtil.filterIcon(background ?: base, { RadialColorReplacementFilter(listOf(c.background)) }, c)
.paintIcon(c, g, x, y)
IconUtil.scale(base, c, 1 - 2 * MARGIN)
.paintIcon(c, g, x + (MARGIN * iconWidth).toInt(), y + (MARGIN * iconHeight).toInt())
}
/**
* The width of the base icon.
*/
override fun getIconWidth() = base.iconWidth
/**
* The height of the base icon.
*/
override fun getIconHeight() = base.iconHeight
/**
* Holds constants.
*/
companion object {
/**
* The margin around the base image that is filled with background color.
*
* This number is a fraction relative to the base image's size.
*/
const val MARGIN = 4f / 32
/**
* Overlay icon for arrays.
*/
val ARRAY by lazy { OverlayIcon(RandomnessIcons.ARRAY, RandomnessIcons.ARRAY_FILLED) }
/**
* Overlay icon for template references.
*/
val REFERENCE by lazy { OverlayIcon(RandomnessIcons.REFERENCE, RandomnessIcons.REFERENCE_FILLED) }
/**
* Overlay icon for repeated insertion.
*/
val REPEAT by lazy { OverlayIcon(RandomnessIcons.REPEAT, RandomnessIcons.REPEAT_FILLED) }
/**
* Overlay icon for settings.
*/
val SETTINGS by lazy { OverlayIcon(RandomnessIcons.SETTINGS, RandomnessIcons.SETTINGS_FILLED) }
}
}
/**
* An icon with various icons displayed on top of it as overlays.
*
* @property base
* @property overlays
*/
data class OverlayedIcon(val base: Icon, val overlays: List<Icon> = emptyList()) : Icon {
init {
require(base.iconWidth == base.iconHeight) { Bundle("icons.error.base_square") }
require(overlays.all { it.iconWidth == it.iconHeight }) { Bundle("icons.error.overlay_square") }
require(overlays.map { it.iconWidth }.toSet().size <= 1) { Bundle("icons.error.overlay_same_size") }
}
/**
* Returns a copy of this icon that has [icon] as an additional overlay icon.
*
* @param icon the additional overlay icon
* @return a copy of this icon that has [icon] as an additional overlay icon
*/
fun plusOverlay(icon: Icon) = copy(overlays = overlays + icon)
/**
* Paints the scheme icon.
*
* @param c a [Component] to get properties useful for painting
* @param g the graphics context
* @param x the X coordinate of the icon's top-left corner
* @param y the Y coordinate of the icon's top-left corner
*/
override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) {
if (c == null || g == null) return
base.paintIcon(c, g, x, y)
overlays.forEachIndexed { i, overlay ->
val overlaySize = iconWidth.toFloat() / OVERLAYS_PER_ROW
val overlayX = (i % OVERLAYS_PER_ROW * overlaySize).toInt()
val overlayY = (i / OVERLAYS_PER_ROW * overlaySize).toInt()
IconUtil.scale(overlay, null, overlaySize / overlay.iconWidth).paintIcon(c, g, overlayX, overlayY)
}
}
/**
* The width of the base icon.
*/
override fun getIconWidth() = base.iconWidth
/**
* The height of the base icon.
*/
override fun getIconHeight() = base.iconHeight
/**
* Holds constants.
*/
companion object {
/**
* Number of overlays displayed per row.
*/
const val OVERLAYS_PER_ROW = 2
}
}
/**
* Replaces all colors with one of [colors] depending on the angle relative to [center].
*
* @property colors The colors that should be used, in clockwise order starting north-west.
* @property center The center relative to which colors should be calculated; not required if only one color is given.
*/
class RadialColorReplacementFilter(
private val colors: List<Color>,
private val center: Pair<Int, Int>? = null
) : RGBImageFilter() {
init {
require(colors.isNotEmpty()) { Bundle("icons.error.one_colour") }
require(colors.size == 1 || center != null) { Bundle("icons.error.center_undefined") }
}
/**
* Returns the color to be displayed at ([x], [y]), considering the coordinates relative to the [center] and the
* relative alpha of the encountered color.
*
* @param x the X coordinate of the pixel
* @param y the Y coordinate of the pixel
* @param rgb `0` if and only if the pixel's color should be replaced
* @return `0` if [rgb] is `0`, or one of [colors] with its alpha shifted by [rgb]'s alpha otherwise
*/
override fun filterRGB(x: Int, y: Int, rgb: Int) =
if (rgb == 0) 0
else if (center == null || colors.size == 1) shiftAlpha(colors[0], Color(rgb, true)).rgb
else shiftAlpha(positionToColor(Pair(x - center.first, y - center.second)), Color(rgb, true)).rgb
/**
* Returns [toShift] which has its alpha multiplied by that of [shiftBy].
*
* @param toShift the color of which to shift the alpha
* @param shiftBy the color which has the alpha to shift by
* @return [toShift] which has its alpha multiplied by that of [shiftBy]
*/
private fun shiftAlpha(toShift: Color, shiftBy: Color) =
ColorUtil.withAlpha(toShift, asFraction(toShift.alpha) * asFraction(shiftBy.alpha))
/**
* Represents an integer in the range `[0, 256)` to a fraction of that range.
*
* @param number the number to represent as a fraction
* @return number as a fraction
*/
private fun asFraction(number: Int) = number / COMPONENT_MAX.toDouble()
/**
* Converts an offset to the [center] to a color in [colors].
*
* @param offset the offset to get the color for
* @return the color to be displayed at [offset]
*/
private fun positionToColor(offset: Pair<Int, Int>): Color {
val angle = 2 * Math.PI - (atan2(offset.second.toDouble(), offset.first.toDouble()) + STARTING_ANGLE)
val index = angle / (2 * Math.PI / colors.size)
return colors[Math.floorMod(index.toInt(), colors.size)]
}
/**
* Holds constants.
*/
companion object {
/**
* Maximum value for an RGB component.
*/
const val COMPONENT_MAX = 255
/**
* The angle in radians at which the first color should start being displayed.
*/
const val STARTING_ANGLE = -(3 * Math.PI / 4)
}
}