diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index 7993b6c..d7eb03e 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -79,6 +79,7 @@ import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/
import MinecraftCrashModal from '@/components/ui/MinecraftCrashModal.vue'
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
import CurseForgeManualDownloadsModal from '@/components/ui/modal/CurseForgeManualDownloadsModal.vue'
+import TaggedModDownloadsModal from '@/components/instance/TaggedModDownloadsModal.vue'
import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
import InstanceIconPickerModal from '@/components/ui/modal/InstanceIconPickerModal.vue'
import JavaDownloadConfirmationModal from '@/components/ui/modal/JavaDownloadConfirmationModal.vue'
@@ -2569,6 +2570,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@install="handleContentInstallModpackInstall"
@cancel="handleContentInstallModpackInstallCancel"
/>
+
import { DownloadIcon } from '@modrinth/assets'
-import { defineMessages, useVIntl } from '@modrinth/ui'
+import { defineMessages, ProgressBar, useVIntl } from '@modrinth/ui'
import { computed, onUnmounted, ref } from 'vue'
import { loading_listener } from '@/helpers/events'
import type { LoadingBar } from '@/helpers/state'
+import { progress_bars_list } from '@/helpers/state'
const { formatMessage } = useVIntl()
@@ -15,6 +15,7 @@ const messages = defineMessages({
})
interface LoadingEventPayload {
+ total?: number | null
event: LoadingBar['bar_type']
loader_uuid: string
fraction: number | null
@@ -22,6 +23,7 @@ interface LoadingEventPayload {
}
interface LaunchProgressItem {
+ waiting: boolean
key: string
message: string
fraction: number | null
@@ -29,6 +31,7 @@ interface LaunchProgressItem {
// 启动相关(以及准备)阶段会发的 loading 类型;安装/下载也一并显示,便于用户看到进度
const LAUNCH_BAR_TYPES = new Set([
+ 'hosted_pack_sync',
'minecraft_download',
'instance_update',
'zip_extract',
@@ -54,6 +57,7 @@ function applyEvent(payload: LoadingEventPayload) {
if (!isVisible(payload.event)) return
activeMap.set(payload.loader_uuid, {
+ waiting: payload.total === 0,
key: payload.loader_uuid,
message: payload.message,
fraction: payload.fraction,
@@ -64,13 +68,28 @@ function applyEvent(payload: LoadingEventPayload) {
const hasProgress = computed(() => progressItems.value.length > 0)
function percent(item: LaunchProgressItem): string {
- if (item.fraction == null || !Number.isFinite(item.fraction)) return ''
+ if (item.waiting || item.fraction == null || !Number.isFinite(item.fraction)) return ''
return `${Math.round(Math.max(0, Math.min(1, item.fraction)) * 100)}%`
}
+let initializing = true
+const buffered: LoadingEventPayload[] = []
const unlistenLoading = await loading_listener((payload: LoadingEventPayload) => {
- applyEvent(payload)
+ if (initializing) buffered.push(payload)
+ else applyEvent(payload)
})
+const bars = await progress_bars_list().catch(() => ({}))
+for (const bar of Object.values(bars)) {
+ applyEvent({
+ event: bar.bar_type,
+ loader_uuid: String(bar.loading_bar_uuid),
+ fraction: bar.total ? (bar.current ?? 0) / bar.total : 0,
+ total: bar.total,
+ message: bar.message ?? '',
+ })
+}
+initializing = false
+for (const payload of buffered) applyEvent(payload)
onUnmounted(() => {
unlistenLoading?.()
@@ -96,12 +115,7 @@ onUnmounted(() => {
{{ percent(item) }}
-
+
diff --git a/apps/app-frontend/src/components/instance/HostedModpacks.vue b/apps/app-frontend/src/components/instance/HostedModpacks.vue
index 3ed031f..46ef409 100644
--- a/apps/app-frontend/src/components/instance/HostedModpacks.vue
+++ b/apps/app-frontend/src/components/instance/HostedModpacks.vue
@@ -39,7 +39,7 @@
})
}}
- {{ formatMessage(messages.syncing) }}
+
- {{ formatMessage(messages.empty) }}
+
@@ -73,16 +72,7 @@
@@ -92,28 +82,29 @@
+
+
+
+
+
+
+
+ {{ formatMessage(messages.downloads) }}
+
+
+
diff --git a/apps/app-frontend/src/components/instance/InstanceModeOptions.vue b/apps/app-frontend/src/components/instance/InstanceModeOptions.vue
index 274ee2e..a889d62 100644
--- a/apps/app-frontend/src/components/instance/InstanceModeOptions.vue
+++ b/apps/app-frontend/src/components/instance/InstanceModeOptions.vue
@@ -15,7 +15,7 @@ const messages = defineMessages({
starlightDescription: {
id: 'app.instance-mode.starlight-description',
defaultMessage:
- 'Automatically syncs changes published by the StarLight server. Required for playing on StarLight. Select and install the official modpack in Mod management before your first launch.',
+ 'Required for playing on StarLight. Automatically installs the server-selected modpack, Minecraft version, and loader. Every launch requires a StarLight login and checks for updates before starting.',
},
localDescription: {
id: 'app.instance-mode.local-description',
diff --git a/apps/app-frontend/src/components/instance/InstanceModeSettings.vue b/apps/app-frontend/src/components/instance/InstanceModeSettings.vue
index 737f7b3..85b9d2a 100644
--- a/apps/app-frontend/src/components/instance/InstanceModeSettings.vue
+++ b/apps/app-frontend/src/components/instance/InstanceModeSettings.vue
@@ -11,7 +11,10 @@ const query = useInstanceMode(() => props.instanceId)
const save = useSetInstanceMode()
const { formatMessage } = useVIntl()
const messages = defineMessages({
- saving: { id: 'app.instance-mode.saving', defaultMessage: 'Saving instance type…' },
+ saving: {
+ id: 'app.instance-mode.saving',
+ defaultMessage: 'Applying instance type and installing the server modpack when needed…',
+ },
loading: { id: 'app.instance-mode.loading', defaultMessage: 'Loading instance type…' },
retry: { id: 'app.instance-mode.retry', defaultMessage: 'Retry' },
})
diff --git a/apps/app-frontend/src/components/instance/TaggedModDownloadsModal.vue b/apps/app-frontend/src/components/instance/TaggedModDownloadsModal.vue
new file mode 100644
index 0000000..8c4877a
--- /dev/null
+++ b/apps/app-frontend/src/components/instance/TaggedModDownloadsModal.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
{{ formatMessage(messages.description) }}
+
+ {{ group.name }}
+ {{ group.error }}
+
+ {{ group.message }}
+
+
+
+ {{ file.name }}
+ {{ size(file.current) }} / {{ size(file.total) }}
+
+
{{ file.error }}
+
+
+
+
+
+
+ {{ formatMessage(messages.downloads) }}
+
+ {{ formatMessage(messages.close) }}
+
+
+
diff --git a/apps/app-frontend/src/components/ui/AppActionBar.vue b/apps/app-frontend/src/components/ui/AppActionBar.vue
index b963bf8..9b17eef 100644
--- a/apps/app-frontend/src/components/ui/AppActionBar.vue
+++ b/apps/app-frontend/src/components/ui/AppActionBar.vue
@@ -312,6 +312,7 @@ interface RunningProcess {
}
interface LoadingEventPayload {
+ total?: number | null
event: LoadingBar['bar_type']
loader_uuid: string
fraction: number | null
@@ -435,7 +436,6 @@ const unlistenProcess = await process_listener(async () => {
const stop = async (process: RunningProcess) => {
try {
await killProcess(process.uuid).catch(handleError)
-
} catch (e) {
console.error(e)
}
@@ -629,6 +629,7 @@ function isVisibleLoadingBar(loadingBar: LoadingBar): boolean {
return (
loadingBar.bar_type?.type !== 'launcher_update' &&
[
+ 'hosted_pack_sync',
'java_download',
'pack_file_download',
'pack_download',
@@ -655,8 +656,8 @@ function applyLoadingEvent(payload: LoadingEventPayload): boolean {
const loadingBar = formatLoadingBars({
loading_bar_uuid: payload.loader_uuid,
message: payload.message,
- current: payload.fraction,
- total: 1,
+ current: payload.fraction * (payload.total ?? 1),
+ total: payload.total ?? 1,
bar_type: payload.event,
})
if (!isVisibleLoadingBar(loadingBar)) return false
diff --git a/apps/app-frontend/src/components/ui/SkinSiteSessionFrame.vue b/apps/app-frontend/src/components/ui/SkinSiteSessionFrame.vue
index 9ea4a3a..fbf0083 100644
--- a/apps/app-frontend/src/components/ui/SkinSiteSessionFrame.vue
+++ b/apps/app-frontend/src/components/ui/SkinSiteSessionFrame.vue
@@ -1,18 +1,31 @@
diff --git a/apps/app-frontend/src/components/ui/about-scene/wither-shield.ts b/apps/app-frontend/src/components/ui/about-scene/wither-shield.ts
new file mode 100644
index 0000000..7912b3a
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/about-scene/wither-shield.ts
@@ -0,0 +1,274 @@
+type Point = [number, number]
+type Quad = [Point, Point, Point, Point]
+type ShieldSurface = { plane: Quad; outline?: Point[]; shade?: number }
+
+// Coordinates follow the sharp master with its localized shoulder cleanup,
+// normalized to the scene's 2048 × 682⅔ plate.
+// Each visible bone has its own face: the air between ribs must never receive armor.
+export const witherShieldSurfaces: ShieldSurface[] = [
+ // Only the upper chest remains; the removed side beams have no shield surfaces.
+ {
+ plane: [
+ [1194, 479],
+ [1294, 489],
+ [1283, 516],
+ [1183, 505],
+ ],
+ },
+ // Far central skull: face, jaw wall and right side share the same edges.
+ {
+ plane: [
+ [1162, 355],
+ [1319, 352],
+ [1267, 446],
+ [1108, 442],
+ ],
+ },
+ {
+ plane: [
+ [1108, 442],
+ [1267, 446],
+ [1280, 489],
+ [1119, 470],
+ ],
+ shade: 0.68,
+ },
+ {
+ plane: [
+ [1319, 352],
+ [1356, 443],
+ [1319, 492],
+ [1267, 446],
+ ],
+ shade: 0.6,
+ },
+ // Near-left skull, underneath the boot.
+ {
+ plane: [
+ [794, 477],
+ [968, 454],
+ [1003, 549],
+ [833, 602],
+ ],
+ },
+ {
+ plane: [
+ [794, 477],
+ [833, 602],
+ [821, 625],
+ [781, 539],
+ ],
+ shade: 0.55,
+ },
+ {
+ plane: [
+ [833, 602],
+ [1003, 549],
+ [983, 587],
+ [821, 625],
+ ],
+ shade: 0.65,
+ },
+ // Right skull.
+ {
+ plane: [
+ [1482, 399],
+ [1639, 469],
+ [1568, 582],
+ [1402, 507],
+ ],
+ },
+ {
+ plane: [
+ [1639, 469],
+ [1650, 557],
+ [1597, 631],
+ [1568, 582],
+ ],
+ shade: 0.57,
+ },
+ {
+ plane: [
+ [1402, 507],
+ [1568, 582],
+ [1597, 631],
+ [1414, 583],
+ ],
+ shade: 0.66,
+ outline: [
+ [1402, 507],
+ [1568, 582],
+ [1597, 631],
+ [1561, 637],
+ [1444, 621],
+ [1414, 583],
+ ],
+ },
+ // Rear rib, left and right of the chest opening.
+ {
+ plane: [
+ [1021, 505],
+ [1129, 484],
+ [1118, 501],
+ [1009, 520],
+ ],
+ },
+ {
+ plane: [
+ [1009, 520],
+ [1118, 501],
+ [1112, 515],
+ [1006, 535],
+ ],
+ shade: 0.65,
+ },
+ {
+ plane: [
+ [1191, 486],
+ [1300, 510],
+ [1289, 530],
+ [1181, 510],
+ ],
+ },
+ {
+ plane: [
+ [1289, 530],
+ [1300, 510],
+ [1310, 611],
+ [1296, 621],
+ ],
+ shade: 0.6,
+ },
+ // Middle rib and broken sternum: separate profiles preserve the dark gaps.
+ {
+ plane: [
+ [1052, 514],
+ [1159, 519],
+ [1129, 553],
+ [992, 548],
+ ],
+ outline: [
+ [1015, 535],
+ [1052, 520],
+ [1094, 522],
+ [1110, 514],
+ [1159, 519],
+ [1129, 553],
+ [992, 548],
+ ],
+ },
+ {
+ plane: [
+ [992, 548],
+ [1129, 553],
+ [1137, 575],
+ [987, 567],
+ ],
+ shade: 0.6,
+ },
+ {
+ plane: [
+ [1159, 519],
+ [1170, 560],
+ [1137, 575],
+ [1129, 553],
+ ],
+ shade: 0.55,
+ },
+ {
+ plane: [
+ [1180, 513],
+ [1289, 551],
+ [1274, 575],
+ [1159, 538],
+ ],
+ },
+ {
+ plane: [
+ [1274, 575],
+ [1289, 551],
+ [1298, 617],
+ [1279, 632],
+ ],
+ shade: 0.62,
+ },
+ // Closest right rib; its downturned end stops above the ground.
+ {
+ plane: [
+ [1157, 542],
+ [1261, 581],
+ [1243, 602],
+ [1141, 564],
+ ],
+ },
+ {
+ plane: [
+ [1141, 564],
+ [1243, 602],
+ [1255, 641],
+ [1148, 605],
+ ],
+ shade: 0.7,
+ },
+ {
+ plane: [
+ [1243, 602],
+ [1261, 581],
+ [1270, 627],
+ [1255, 641],
+ ],
+ shade: 0.55,
+ },
+ // Foreground rib and spine.
+ {
+ plane: [
+ [1000, 566],
+ [1140, 581],
+ [1107, 604],
+ [960, 589],
+ ],
+ },
+ {
+ plane: [
+ [960, 589],
+ [1107, 604],
+ [1112, 680],
+ [968, 622],
+ ],
+ shade: 0.6,
+ outline: [
+ [960, 589],
+ [1107, 604],
+ [1112, 680],
+ [1070, 681],
+ [1066, 619],
+ [968, 611],
+ ],
+ },
+ {
+ plane: [
+ [1107, 604],
+ [1140, 581],
+ [1160, 669],
+ [1112, 680],
+ ],
+ shade: 0.5,
+ },
+ {
+ plane: [
+ [986, 611],
+ [1070, 617],
+ [967, 684],
+ [874, 671],
+ ],
+ },
+ {
+ plane: [
+ [1070, 617],
+ [1074, 668],
+ [1060, 684],
+ [967, 684],
+ ],
+ shade: 0.55,
+ },
+]
diff --git a/apps/app-frontend/src/components/ui/about-scene/wither-victory.ts b/apps/app-frontend/src/components/ui/about-scene/wither-victory.ts
index 7e0944d..02bbd25 100644
--- a/apps/app-frontend/src/components/ui/about-scene/wither-victory.ts
+++ b/apps/app-frontend/src/components/ui/about-scene/wither-victory.ts
@@ -4,6 +4,10 @@ import swordTextureUrl from '@/assets/about-scene/netherite-sword.png'
import cleanUrl from '@/assets/about-scene/victory-clean.jpg'
import originalUrl from '@/assets/about-scene/victory-master.jpg'
import correctedPoseUrl from '@/assets/about-scene/victory-pose.jpg'
+import removedBeamsUrl from '@/assets/about-scene/wither-beams-removed.png'
+import rebuiltWitherUrl from '@/assets/about-scene/wither-shoulder-cleanup.png'
+
+import { witherShieldSurfaces } from './wither-shield'
type Point = [number, number]
type Vertex = [number, number, number]
@@ -17,11 +21,16 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
const original = new Image()
const clean = new Image()
const correctedPose = new Image()
+ const rebuiltWither = new Image()
+ const removedBeams = new Image()
const swordTexture = new Image()
const cloudTexture = new Image()
const moonTexture = new Image()
const W = 2048,
H = W / 3,
+ artworkWidth = 2172,
+ artworkHeight = 724,
+ artworkScale = artworkWidth / W,
TAU = Math.PI * 2
let time = 0,
last = 0,
@@ -36,7 +45,8 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
layer.height = Math.ceil(h)
return layer
}
- const base = makeLayer()
+ const base = makeLayer(artworkWidth, artworkHeight)
+ const shield = makeLayer(artworkWidth, artworkHeight)
const star = makeLayer(180, 180)
const starFace = makeLayer(180, 180)
const swordFace = makeLayer(128, 128)
@@ -87,86 +97,6 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
I: '#fff6da',
S: '#fffef1',
}
- const shieldSurfaces: Point[][] = [
- [
- [986, 442],
- [1124, 466],
- [1096, 541],
- [982, 532],
- ],
- [
- [1110, 462],
- [1304, 482],
- [1258, 625],
- [1000, 605],
- ],
- [
- [1304, 482],
- [1379, 504],
- [1390, 614],
- [1258, 625],
- ],
- [
- [1328, 481],
- [1428, 424],
- [1472, 446],
- [1390, 523],
- ],
- [
- [1014, 603],
- [1170, 626],
- [1136, 683],
- [869, 683],
- ],
- [
- [792, 481],
- [950, 451],
- [987, 550],
- [835, 604],
- ],
- [
- [792, 481],
- [835, 604],
- [821, 625],
- [780, 533],
- ],
- [
- [835, 604],
- [987, 550],
- [974, 589],
- [821, 625],
- ],
- [
- [1165, 357],
- [1301, 355],
- [1247, 471],
- [1108, 441],
- ],
- [
- [1301, 355],
- [1358, 419],
- [1311, 500],
- [1247, 471],
- ],
- [
- [1483, 398],
- [1626, 470],
- [1558, 590],
- [1404, 514],
- ],
- [
- [1626, 470],
- [1655, 549],
- [1606, 637],
- [1558, 590],
- ],
- [
- [1404, 514],
- [1558, 590],
- [1606, 637],
- [1449, 592],
- ],
- ]
// Irregular failing-lamp events: brief reignitions, weak sputters, and dark gaps.
const shieldEvents = [
[0, 0.68, 0.48],
@@ -259,10 +189,12 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
function prepare() {
// Restore the sharp master for all unaffected materials, anatomy and terrain.
const b = base.getContext('2d')!
+ b.setTransform(artworkScale, 0, 0, artworkScale, 0, 0)
b.imageSmoothingEnabled = false
b.drawImage(original, 0, 0, W, H)
- const sky = makeLayer(),
+ const sky = makeLayer(artworkWidth, artworkHeight),
skyCtx = sky.getContext('2d')!
+ skyCtx.setTransform(artworkScale, 0, 0, artworkScale, 0, 0)
skyCtx.drawImage(clean, 0, 0, W, H)
skyCtx.globalCompositeOperation = 'destination-in'
const skyFade = skyCtx.createLinearGradient(0, 409, 0, 448)
@@ -270,7 +202,7 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
skyFade.addColorStop(1, 'transparent')
skyCtx.fillStyle = skyFade
skyCtx.fillRect(0, 0, W, 442)
- b.drawImage(sky, 0, 0)
+ b.drawImage(sky, 0, 0, W, H)
const restore = (source: HTMLImageElement, polygon: Point[]) => {
b.save()
b.beginPath()
@@ -341,7 +273,7 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
ms.fillRect(-140, -135, 280, 270)
ms.restore()
b.drawImage(moonSky, 193, -20)
- // Use original sharp head faces, preserving their exact silhouette without a sky border.
+ // Restore the sharp skull planes covered by the sky plate.
restore(original, [
[1159, 355],
[1318, 352],
@@ -350,7 +282,7 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
[1246, 474],
[1107, 443],
])
- // Local broken-sternum and old-blade removal; the unaffected ribs stay from the master.
+ // Remove the old painted blade without resampling the rest of the creature.
restore(clean, [
[975, 339],
[1002, 334],
@@ -365,7 +297,7 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
[1053, 502],
[1006, 476],
])
- // Remove the two baked blue lightning remnants without replacing the whole Wither.
+ // Retain the previously approved removal of baked lightning below the skulls.
restore(clean, [
[900, 595],
[948, 572],
@@ -386,6 +318,49 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
[1318, 525],
[1320, 470],
])
+ // Use regeneration only to erase the two unwanted shoulder slabs. The skulls,
+ // ribs, character and background retain their original sharp source pixels.
+ restore(rebuiltWither, [
+ [963, 423],
+ [1014, 417],
+ [1049, 431],
+ [1074, 479],
+ [1066, 495],
+ [1017, 511],
+ [985, 524],
+ [963, 466],
+ ])
+ restore(rebuiltWither, [
+ [1360, 415],
+ [1392, 414],
+ [1429, 427],
+ [1448, 445],
+ [1405, 506],
+ [1398, 532],
+ [1355, 528],
+ [1319, 514],
+ [1324, 494],
+ ])
+ // Replace only the removed beam silhouettes with the newly exposed background.
+ // No full-frame regeneration or feathering: unaffected source pixels stay intact.
+ restore(removedBeams, [
+ [976, 480],
+ [1146, 462],
+ [1152, 488],
+ [1124, 508],
+ [999, 530],
+ [990, 542],
+ ])
+ restore(removedBeams, [
+ [1294, 489],
+ [1404, 498],
+ [1445, 548],
+ [1452, 629],
+ [1402, 621],
+ [1305, 615],
+ [1299, 549],
+ [1283, 521],
+ ])
restore(correctedPose, [
[811, 377],
[951, 377],
@@ -673,57 +648,70 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
const level = shieldLevel(t)
canvas.dataset.shieldIntensity = level.toFixed(3)
if (level < 0.003) return
- ctx.save()
- ctx.globalCompositeOperation = 'screen'
+ const surface = shield.getContext('2d')!
+ surface.setTransform(artworkScale, 0, 0, artworkScale, 0, 0)
+ surface.clearRect(0, 0, W, H + 1)
+ surface.save()
+ surface.globalCompositeOperation = 'source-over'
+ const trace = (points: Point[]) => {
+ surface.beginPath()
+ points.forEach((p, i) => (i ? surface.lineTo(...p) : surface.moveTo(...p)))
+ surface.closePath()
+ }
// Tight fractured-cavity contour: blue armor remains on the surrounding ribs.
- ctx.beginPath()
- ctx.rect(0, 0, W, H)
- wound.forEach((p, i) => (i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])))
- ctx.closePath()
- ctx.clip('evenodd')
- shieldSurfaces.forEach((quad, index) => {
+ surface.beginPath()
+ surface.rect(0, 0, W, H)
+ wound.forEach((p, i) => (i ? surface.lineTo(p[0], p[1]) : surface.moveTo(p[0], p[1])))
+ surface.closePath()
+ surface.clip('evenodd')
+ witherShieldSurfaces.forEach(({ plane: quad, outline = quad, shade = 1 }, index) => {
const map = (u: number, v: number): Point => [
(1 - v) * ((1 - u) * quad[0][0] + u * quad[1][0]) +
v * ((1 - u) * quad[3][0] + u * quad[2][0]),
(1 - v) * ((1 - u) * quad[0][1] + u * quad[1][1]) +
v * ((1 - u) * quad[3][1] + u * quad[2][1]),
]
- const alpha = level * (0.79 + 0.21 * rand(index + Math.floor(t * 7)))
- path(quad)
- ctx.fillStyle = `rgba(55,126,203,${alpha * 0.57})`
- ctx.fill()
- ctx.save()
- path(quad)
- ctx.clip()
+ const alpha = level * shade * (0.79 + 0.21 * rand(index + Math.floor(t * 7)))
+ trace(outline)
+ surface.fillStyle = `rgba(55,126,203,${alpha * 0.38})`
+ surface.fill()
+ surface.save()
+ trace(outline)
+ surface.clip()
for (let row = 0; row < 8; row++)
for (let col = 0; col < 8; col++) {
const grain = rand(row * 17 + col * 5 + index * 139)
if (grain < 0.56) continue
- path([
+ trace([
map(col / 8, row / 8),
map((col + 1) / 8, row / 8),
map((col + 1) / 8, (row + 1) / 8),
map(col / 8, (row + 1) / 8),
])
- ctx.fillStyle = `rgba(104,167,223,${alpha * (grain - 0.4) * 0.42})`
- ctx.fill()
+ surface.fillStyle = `rgba(104,167,223,${alpha * (grain - 0.4) * 0.42})`
+ surface.fill()
}
// Stepped translucent bands wrap each actual surface, like the supplied armor image.
for (let band = -1; band < 3; band++)
for (let col = 0; col < 8; col++) {
const v = band * 0.48 + (t / 24) * 0.48 + Math.floor(rand(col + index * 11) * 3) / 32
const height = 0.034 + rand(col * 3 + band + index) * 0.022
- path([
+ trace([
map(col / 8, v),
map((col + 1) / 8, v),
map((col + 1) / 8, v + height),
map(col / 8, v + height),
])
- ctx.fillStyle = `rgba(210,229,172,${alpha * 0.75})`
- ctx.fill()
+ surface.fillStyle = `rgba(210,229,172,${alpha * 0.75})`
+ surface.fill()
}
- ctx.restore()
+ surface.restore()
})
+ surface.restore()
+ // Composite the fitted faces once, without bright overlaps at shared bone edges.
+ ctx.save()
+ ctx.globalCompositeOperation = 'screen'
+ ctx.drawImage(shield, 0, 0, W, H)
ctx.restore()
}
function drawClouds(elapsed: number) {
@@ -773,9 +761,10 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
ctx.clearRect(0, 0, W, H)
const cycle = (t * TAU) / 24
ctx.save()
- ctx.translate(W / 2, H / 2)
- ctx.scale(1.008, 1.008)
- ctx.translate(-W / 2 + Math.sin(cycle) * 1.6, -H / 2 + Math.sin(cycle * 2) * 0.7)
+ // Keep the static plate pixel-stable; animate only the independent effects.
+ // The full-resolution master is sampled once, without a floating camera zoom.
+ ctx.imageSmoothingEnabled = true
+ ctx.imageSmoothingQuality = 'high'
ctx.drawImage(base, 0, 0, W, H)
drawMoon()
drawClouds(elapsed)
@@ -883,7 +872,10 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
if (destroyed) return
const width = canvas.getBoundingClientRect().width
if (width <= 0) return
- canvas.width = Math.min(W, Math.max(1, Math.round(width * Math.min(2, devicePixelRatio || 1))))
+ canvas.width = Math.min(
+ artworkWidth,
+ Math.max(1, Math.round(width * Math.min(2, devicePixelRatio || 1))),
+ )
canvas.height = Math.round(canvas.width / 3)
cloudField?.resize(canvas.width, canvas.height)
if (ready) draw(time)
@@ -903,7 +895,16 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
img.onerror = () => reject(new Error('Unable to load about-page artwork'))
img.src = url
})
- const imageAssets = [original, clean, swordTexture, correctedPose, cloudTexture, moonTexture]
+ const imageAssets = [
+ original,
+ clean,
+ swordTexture,
+ correctedPose,
+ cloudTexture,
+ moonTexture,
+ rebuiltWither,
+ removedBeams,
+ ]
const loading = Promise.all([
loaded(original, originalUrl),
loaded(clean, cleanUrl),
@@ -911,6 +912,8 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
loaded(correctedPose, correctedPoseUrl),
loaded(cloudTexture, cloudTextureUrl),
loaded(moonTexture, moonTextureUrl),
+ loaded(rebuiltWither, rebuiltWitherUrl),
+ loaded(removedBeams, removedBeamsUrl),
])
.then(() => {
if (destroyed) return
@@ -941,7 +944,7 @@ export function createWitherVictoryScene(canvas: HTMLCanvasElement) {
reducedMotion.removeEventListener('change', updatePlayback)
cloudField?.dispose()
cloudField = undefined
- for (const layer of [base, star, starFace, swordFace, grippingFingers]) {
+ for (const layer of [base, shield, star, starFace, swordFace, grippingFingers]) {
layer.width = 1
layer.height = 1
}
diff --git a/apps/app-frontend/src/components/ui/easteregg/color-mine/ColorMineAvatar.vue b/apps/app-frontend/src/components/ui/easteregg/color-mine/ColorMineAvatar.vue
deleted file mode 100644
index 1a9014d..0000000
--- a/apps/app-frontend/src/components/ui/easteregg/color-mine/ColorMineAvatar.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/easteregg/color-mine/ColorMineBoard.vue b/apps/app-frontend/src/components/ui/easteregg/color-mine/ColorMineBoard.vue
index 9ccb771..c1cb7af 100644
--- a/apps/app-frontend/src/components/ui/easteregg/color-mine/ColorMineBoard.vue
+++ b/apps/app-frontend/src/components/ui/easteregg/color-mine/ColorMineBoard.vue
@@ -1,6 +1,6 @@