From 6326191189a79bc3fd2c743c839acb295dcba736 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Sat, 24 Aug 2024 12:08:59 +0800 Subject: [PATCH 01/25] [edit] fix some item not have info or islot and vslot --- src/renderer/character/item.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/renderer/character/item.ts b/src/renderer/character/item.ts index 8da1c9c..6cfa2a4 100644 --- a/src/renderer/character/item.ts +++ b/src/renderer/character/item.ts @@ -206,8 +206,9 @@ export class CharacterItem implements RenderItemInfo { return; } - this.islot = (this.wz.info.islot.match(/.{1,2}/g) || []) as PieceIslot[]; - this.vslot = (this.wz.info.vslot.match(/.{1,2}/g) || []) as PieceIslot[]; + /* some item will not have info, WTF? */ + this.islot = (this.wz.info?.islot?.match(/.{1,2}/g) || []) as PieceIslot[]; + this.vslot = (this.wz.info?.vslot?.match(/.{1,2}/g) || []) as PieceIslot[]; /* resolve dye */ if (this.isFace && this.avaliableDye.size === 0) { From 14105e4982a382994e2a508990af36287217207d Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Sun, 25 Aug 2024 19:29:16 +0800 Subject: [PATCH 02/25] [edit] fix isDyeable filter infacting other category --- src/store/equipDrawer.ts | 46 ++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/store/equipDrawer.ts b/src/store/equipDrawer.ts index 89a660d..8f0deb3 100644 --- a/src/store/equipDrawer.ts +++ b/src/store/equipDrawer.ts @@ -91,8 +91,9 @@ export const $categoryFilteredString = computed( $equipmentDrawerEquipTab, $currentEquipmentDrawerCategory, $equipmentStrings, + $equipmentDrawerOnlyShowDyeable, ], - (tab, category, strings) => { + (tab, category, strings, onlyShowDyeable) => { if (tab === EquipTab.History) { /* not subscribe $equipmentHistory here */ return $equipmentHistory.get(); @@ -116,41 +117,36 @@ export const $categoryFilteredString = computed( }); } - if (category === AllCategory) { - return strings; + let filteredStrings = strings; + + if (category !== AllCategory) { + const mainCategory = getCategoryBySubCategory(category); + filteredStrings = strings.filter((item) => { + if (item.category === mainCategory) { + return getSubCategory(item.id) === category; + } + return false; + }); } - const mainCategory = getCategoryBySubCategory(category); - return strings.filter((item) => { - if (item.category === mainCategory) { - return getSubCategory(item.id) === category; - } - return false; - }); + if (!onlyShowDyeable) { + return filteredStrings; + } + + return strings.filter(({ isDyeable }) => isDyeable); }, ); export const $equipmentDrawerEquipFilteredString = computed( - [ - $categoryFilteredString, - $currentEquipmentDrawerSearch, - $equipmentDrawerOnlyShowDyeable, - ], - (strings, searchKey, onlyShowDyeable) => { + [$categoryFilteredString, $currentEquipmentDrawerSearch], + (strings, searchKey) => { if (searchKey) { return strings.filter((item) => { const isMatch = item.name.includes(searchKey) || item.id.toString() === searchKey; - if (!onlyShowDyeable) { - return isMatch; - } - return isMatch && item.isDyeable; + return isMatch; }); } - if (!onlyShowDyeable) { - return strings; - } - - return strings.filter(({ isDyeable }) => isDyeable); + return strings; }, ); From d26c718106562c5e462e0f965a1a198824472d01 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Sun, 25 Aug 2024 19:29:40 +0800 Subject: [PATCH 03/25] [edit] try to force weird shoe locks --- src/renderer/character/character.ts | 102 ++++++++++------------------ src/renderer/character/item.ts | 6 ++ 2 files changed, 42 insertions(+), 66 deletions(-) diff --git a/src/renderer/character/character.ts b/src/renderer/character/character.ts index f75c988..741dbe1 100644 --- a/src/renderer/character/character.ts +++ b/src/renderer/character/character.ts @@ -5,10 +5,7 @@ import type { CharacterData } from '@/store/character/store'; import type { ItemInfo, AncherName, Vec2, PieceSlot } from './const/data'; import type { CategorizedItem, CharacterActionItem } from './categorizedItem'; import type { CharacterAnimatablePart } from './characterAnimatablePart'; -import type { - CharacterItemPiece, - DyeableCharacterItemPiece, -} from './itemPiece'; +import type { CharacterItemPiece, DyeableCharacterItemPiece } from './itemPiece'; import { CharacterLoader } from './loader'; import { CharacterItem } from './item'; @@ -44,8 +41,7 @@ class ZmapContainer extends Container { } else if (this.name === 'pants' || this.name === 'backPants') { this.requireLocks = ['Pn']; } else { - this.requireLocks = - (CharacterLoader.smap?.[name] || '').match(/.{1,2}/g) || []; + this.requireLocks = (CharacterLoader.smap?.[name] || '').match(/.{1,2}/g) || []; } } addCharacterPart(child: CharacterAnimatablePart) { @@ -70,13 +66,14 @@ class ZmapContainer extends Container { // ? frame.item.vslot // : this.requireLocks; let locks = this.requireLocks; + let itemMainLocks = part.item.islot; // something like Ma, Pn,Cp // force Cap using vslot - if (part.item.islot.includes('Cp')) { + if (itemMainLocks.includes('Cp')) { locks = part.item.vslot; } else if ( - part.item.islot.length === 1 && - part.item.islot[0] === 'Hd' && + itemMainLocks.length === 1 && + itemMainLocks[0] === 'Hd' && (this.name === 'accessoryOverHair' || this.name === 'hairShade') ) { /* try to fix ear rendering */ @@ -86,6 +83,8 @@ class ZmapContainer extends Container { // this logic is from maplestory.js, but why if (this.name === 'mailChest') { locks = part.item.vslot; + } else if (this.name === 'shoes') { + itemMainLocks = ['So']; } const hasSelfLock = this.hasAllLocks(part.item.info.id, part.item.vslot); @@ -185,9 +184,7 @@ export class Character extends Container { } this.isAnimating = characterData.isAnimating; const hasAttributeChanged = this.updateAttribute(characterData); - const hasAddAnyItem = await this.updateItems( - Object.values(characterData.items), - ); + const hasAddAnyItem = await this.updateItems(Object.values(characterData.items)); if (hasAttributeChanged || hasAddAnyItem || isStopToPlay) { await this.loadItems(); } else if (isPlayingChanged) { @@ -255,17 +252,13 @@ export class Character extends Container { return; } item.info = Object.assign({}, info, { - dye: (info as unknown as ItemInfo & { isDeleteDye: boolean }).isDeleteDye - ? undefined - : info.dye, + dye: (info as unknown as ItemInfo & { isDeleteDye: boolean }).isDeleteDye ? undefined : info.dye, }); /* only update sprite already in render */ const dyeableSprites = this.currentAllItem .flatMap((item) => Array.from(item.allPieces)) .filter((piece) => piece.item.info.id === id) - .flatMap((piece) => - piece.frames.filter((frame) => frame.isDyeable?.()), - ) as DyeableCharacterItemPiece[]; + .flatMap((piece) => piece.frames.filter((frame) => frame.isDyeable?.())) as DyeableCharacterItemPiece[]; for await (const sprites of dyeableSprites) { await sprites.updateDye(); } @@ -275,18 +268,14 @@ export class Character extends Container { get currentAllItem() { return Array.from(this.idItems.values()) .map((item) => { - return item.isUseExpressionItem - ? item.actionPieces.get(this.expression) - : item.actionPieces.get(this.action); + return item.isUseExpressionItem ? item.actionPieces.get(this.expression) : item.actionPieces.get(this.action); }) .filter((item) => item) as AnyCategorizedItem[]; } /** get current pieces in character layers */ get currentPieces() { - return Array.from(this.zmapLayers.values()).flatMap( - (layer) => layer.children as CharacterAnimatablePart[], - ); + return Array.from(this.zmapLayers.values()).flatMap((layer) => layer.children as CharacterAnimatablePart[]); } render() { @@ -300,9 +289,7 @@ export class Character extends Container { const earPiece = this.getEarPiece(); const earLayer = earPiece?.firstFrameZmapLayer; for (const layer of zmap) { - const itemsByLayer = this.getItemsByLayer(layer).concat( - earPiece && earLayer === layer ? [earPiece] : [], - ); + const itemsByLayer = this.getItemsByLayer(layer).concat(earPiece && earLayer === layer ? [earPiece] : []); if (itemsByLayer.length === 0) { continue; } @@ -316,10 +303,7 @@ export class Character extends Container { if (existLayer) { container = existLayer; } else { - const zIndex = - piece.effectZindex >= 2 - ? zmap.length + piece.effectZindex - : piece.effectZindex - 10; + const zIndex = piece.effectZindex >= 2 ? zmap.length + piece.effectZindex : piece.effectZindex - 10; container = new ZmapContainer(effectLayerName, zIndex, this); this.addChild(container); this.zmapLayers.set(effectLayerName, container); @@ -329,10 +313,7 @@ export class Character extends Container { this.addChild(container); this.zmapLayers.set(layer, container); } - if ( - isBackAction(this.action) && - layer.toLocaleLowerCase().includes('face') - ) { + if (isBackAction(this.action) && layer.toLocaleLowerCase().includes('face')) { container.visible = false; } if ((layer === 'body' || layer === 'backBody') && piece.item.isBody) { @@ -398,8 +379,7 @@ export class Character extends Container { playByBody(body: CharacterAnimatablePart) { const pieces = this.currentPieces; const maxFrame = body.frames.length; - const needBounce = - this.action === CharacterAction.Alert || this.action.startsWith('stand'); + const needBounce = this.action === CharacterAction.Alert || this.action.startsWith('stand'); this.currentTicker = (delta) => { this.currentDelta += delta.deltaMS; @@ -437,8 +417,7 @@ export class Character extends Container { } for (const piece of pieces) { const pieceFrameIndex = piece.frames[frame] ? frame : 0; - const pieceFrame = (piece.frames[frame] || - piece.frames[0]) as CharacterItemPiece; + const pieceFrame = (piece.frames[frame] || piece.frames[0]) as CharacterItemPiece; const isSkinGroup = pieceFrame.group === 'skin'; if (pieceFrame) { const ancherName = pieceFrame.baseAncherName; @@ -452,7 +431,10 @@ export class Character extends Container { y: 48, }; /* cap effect use brow ancher */ - const browAncher = currentAncher.get('brow') || { x: 0, y: 0 }; + const browAncher = currentAncher.get('brow') || { + x: 0, + y: 0, + }; ancher = { x: baseAncher.x + browAncher.x, y: baseAncher.y + browAncher.y, @@ -515,15 +497,15 @@ export class Character extends Container { get currentFrontBodyNode() { const body = this.zmapLayers.get('body'); - return body?.children.find( - (child) => (child as CharacterAnimatablePart).item.isBody, - ) as CharacterAnimatablePart | undefined; + return body?.children.find((child) => (child as CharacterAnimatablePart).item.isBody) as + | CharacterAnimatablePart + | undefined; } get currentBackBodyNode() { const body = this.zmapLayers.get('backBody'); - return body?.children.find( - (child) => (child as CharacterAnimatablePart).item.isBody, - ) as CharacterAnimatablePart | undefined; + return body?.children.find((child) => (child as CharacterAnimatablePart).item.isBody) as + | CharacterAnimatablePart + | undefined; } get currentBodyNode() { @@ -545,14 +527,10 @@ export class Character extends Container { } get isAllAncherBuilt() { - return Array.from(this.idItems.values()).every( - (item) => item.isAllAncherBuilt, - ); + return Array.from(this.idItems.values()).every((item) => item.isAllAncherBuilt); } get isCurrentActionAncherBuilt() { - return Array.from(this.idItems.values()).every((item) => - item.isActionAncherBuilt(this.action), - ); + return Array.from(this.idItems.values()).every((item) => item.isActionAncherBuilt(this.action)); } get effectLayers() { @@ -568,15 +546,11 @@ export class Character extends Container { } getEarPiece() { - const headItem = Array.from(this.idItems.values()).find( - (item) => item.isHead, - ); + const headItem = Array.from(this.idItems.values()).find((item) => item.isHead); if (!headItem) { return; } - const headCategoryItem = headItem.actionPieces.get( - this.action, - ) as CharacterActionItem; + const headCategoryItem = headItem.actionPieces.get(this.action) as CharacterActionItem; const earItems = headCategoryItem?.getAvailableEar(this.earType); @@ -660,6 +634,7 @@ export class Character extends Container { if (!orderedItems) { return; } + console.log(orderedItems.map((item) => item.info.id)); for (const item of orderedItems) { for (const slot of item.vslot) { this.locks.set(slot, item.info.id); @@ -671,10 +646,7 @@ export class Character extends Container { for (const action of Object.values(CharacterAction)) { for (const item of this.idItems.values()) { const ancher = this.actionAnchers.get(action); - this.actionAnchers.set( - action, - item.tryBuildAncher(action, ancher || []), - ); + this.actionAnchers.set(action, item.tryBuildAncher(action, ancher || [])); } } } @@ -723,14 +695,12 @@ export class Character extends Container { } private updateHandTypeByAction() { if ( - (this.action === CharacterAction.Walk1 || - this.action === CharacterAction.Stand1) && + (this.action === CharacterAction.Walk1 || this.action === CharacterAction.Stand1) && this.#_handType === CharacterHandType.DoubleHand ) { this.#_handType = CharacterHandType.SingleHand; } else if ( - (this.action === CharacterAction.Walk2 || - this.action === CharacterAction.Stand2) && + (this.action === CharacterAction.Walk2 || this.action === CharacterAction.Stand2) && this.#_handType === CharacterHandType.SingleHand ) { this.#_handType = CharacterHandType.DoubleHand; diff --git a/src/renderer/character/item.ts b/src/renderer/character/item.ts index 6cfa2a4..190198f 100644 --- a/src/renderer/character/item.ts +++ b/src/renderer/character/item.ts @@ -22,6 +22,7 @@ import { isHeadId, isBodyId, isCapId, + isShoesId, } from '@/utils/itemId'; import { gatFaceAvailableColorIds, @@ -210,6 +211,11 @@ export class CharacterItem implements RenderItemInfo { this.islot = (this.wz.info?.islot?.match(/.{1,2}/g) || []) as PieceIslot[]; this.vslot = (this.wz.info?.vslot?.match(/.{1,2}/g) || []) as PieceIslot[]; + /* a shoe should alwasy be a shoe! pls */ + if (isShoesId(this.info.id) && !this.islot.includes('So')) { + this.islot = ['So']; + } + /* resolve dye */ if (this.isFace && this.avaliableDye.size === 0) { const ids = gatFaceAvailableColorIds(this.info.id); From 62e9ed10fa1991bca7ebabe28ab895307be5f6e0 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Mon, 26 Aug 2024 16:31:25 +0800 Subject: [PATCH 04/25] [add] testing Anime4kContainer failed, but keep files for now --- src/components/CharacterPreview/Character.tsx | 2 +- src/renderer/character/character.ts | 1 - .../filter/anime4k/Anime4kContainer.ts | 321 ++++++++++++++++++ .../filter/anime4k/Anime4kRenderPipe.ts | 48 +++ 4 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 src/renderer/filter/anime4k/Anime4kContainer.ts create mode 100644 src/renderer/filter/anime4k/Anime4kRenderPipe.ts diff --git a/src/components/CharacterPreview/Character.tsx b/src/components/CharacterPreview/Character.tsx index c61bd6e..448837a 100644 --- a/src/components/CharacterPreview/Character.tsx +++ b/src/components/CharacterPreview/Character.tsx @@ -95,7 +95,7 @@ export const CharacterView = (props: CharacterViewProps) => { ch.reset(); app.destroy(); if (props.target === 'preview') { - setUpscaleSource(app.canvas); + resetUpscaleSource(); } }); diff --git a/src/renderer/character/character.ts b/src/renderer/character/character.ts index 741dbe1..272af40 100644 --- a/src/renderer/character/character.ts +++ b/src/renderer/character/character.ts @@ -634,7 +634,6 @@ export class Character extends Container { if (!orderedItems) { return; } - console.log(orderedItems.map((item) => item.info.id)); for (const item of orderedItems) { for (const slot of item.vslot) { this.locks.set(slot, item.info.id); diff --git a/src/renderer/filter/anime4k/Anime4kContainer.ts b/src/renderer/filter/anime4k/Anime4kContainer.ts new file mode 100644 index 0000000..b0493a7 --- /dev/null +++ b/src/renderer/filter/anime4k/Anime4kContainer.ts @@ -0,0 +1,321 @@ +import { RenderContainer, WebGPURenderer, WebGLRenderer } from 'pixi.js'; + +import type { Anime4KPipeline } from 'anime4k-webgpu'; +import fullscreenTexturedQuadVert from './fullscreenTexturedQuad.wgsl'; +import sampleExternalTextureFrag from './sampleExternalTexture.wgsl'; + +type Anime4kPipelineConstructor = new (desc: { + device: GPUDevice; + inputTexture: GPUTexture; + nativeDimensions?: { width: number; height: number }; + targetDimensions?: { width: number; height: number }; +}) => Anime4KPipeline; +export interface Anime4kPipelineWithOption { + factory: Anime4kPipelineConstructor; + params?: Record<string, number>; +} + +export enum PipelineType { + /** + * deblur, must set param: strength + * + * @param strength Deblur Strength (0.1 - 15.0) + */ + Dog = 'Dog', + /** + * denoise, must set param: strength, strength2 + * + * @param strength Itensity Sigma (0.1 - 2.0) + * @param strength2 Spatial Sigma (1 - 15) + */ + BilateralMean = 'BilateralMean', + + /** restore */ + CNNM = 'CNNM', + /** restore */ + CNNSoftM = 'CNNSoftM', + /** restore */ + CNNSoftVL = 'CNNSoftVL', + /** restore */ + CNNVL = 'CNNVL', + /** restore */ + CNNUL = 'CNNUL', + /** restore */ + GANUUL = 'GANUUL', + + /** upscale */ + CNNx2M = 'CNNx2M', + /** upscale */ + CNNx2VL = 'CNNx2VL', + /** upscale */ + DenoiseCNNx2VL = 'DenoiseCNNx2VL', + /** upscale */ + CNNx2UL = 'CNNx2UL', + /** upscale */ + GANx3L = 'GANx3L', + /** upscale */ + GANx4UUL = 'GANx4UUL', + + ClampHighlights = 'ClampHighlights', + DepthToSpace = 'DepthToSpace', + + /* anime4k preset @see https://github.com/bloc97/Anime4K/blob/master/md/GLSL_Instructions_Advanced.md */ + /** Restore -> Upscale -> Upscale */ + ModeA = 'ModeA', + /** Restore_Soft -> Upscale -> Upscale */ + ModeB = 'ModeB', + /** Upscale_Denoise -> Upscale */ + ModeC = 'ModeC', + /** Restore -> Upscale -> Restore -> Upscale */ + ModeAA = 'ModeAA', + /** Restore_Soft -> Upscale -> Restore_Soft -> Upscale */ + ModeBB = 'ModeBB', + /** Upscale_Denoise -> Restore -> Upscale */ + ModeCA = 'ModeCA', +} + +const PipelineMap: Record<PipelineType, () => Promise<unknown>> = { + [PipelineType.Dog]: () => import('anime4k-webgpu').then((m) => m.DoG), + [PipelineType.BilateralMean]: () => + import('anime4k-webgpu').then((m) => m.BilateralMean), + + [PipelineType.CNNM]: () => import('anime4k-webgpu').then((m) => m.CNNM), + [PipelineType.CNNSoftM]: () => + import('anime4k-webgpu').then((m) => m.CNNSoftM), + [PipelineType.CNNSoftVL]: () => + import('anime4k-webgpu').then((m) => m.CNNSoftVL), + [PipelineType.CNNVL]: () => import('anime4k-webgpu').then((m) => m.CNNVL), + [PipelineType.CNNUL]: () => import('anime4k-webgpu').then((m) => m.CNNUL), + [PipelineType.GANUUL]: () => import('anime4k-webgpu').then((m) => m.GANUUL), + + [PipelineType.CNNx2M]: () => import('anime4k-webgpu').then((m) => m.CNNx2M), + [PipelineType.CNNx2VL]: () => import('anime4k-webgpu').then((m) => m.CNNx2VL), + [PipelineType.DenoiseCNNx2VL]: () => + import('anime4k-webgpu').then((m) => m.DenoiseCNNx2VL), + [PipelineType.CNNx2UL]: () => import('anime4k-webgpu').then((m) => m.CNNx2UL), + [PipelineType.GANx3L]: () => import('anime4k-webgpu').then((m) => m.GANx3L), + [PipelineType.GANx4UUL]: () => + import('anime4k-webgpu').then((m) => m.GANx4UUL), + + [PipelineType.ClampHighlights]: () => + import('anime4k-webgpu').then((m) => m.ClampHighlights), + [PipelineType.DepthToSpace]: () => + import('anime4k-webgpu').then((m) => m.DepthToSpace), + + [PipelineType.ModeA]: () => import('anime4k-webgpu').then((m) => m.ModeA), + [PipelineType.ModeB]: () => import('anime4k-webgpu').then((m) => m.ModeB), + [PipelineType.ModeC]: () => import('anime4k-webgpu').then((m) => m.ModeC), + [PipelineType.ModeAA]: () => import('anime4k-webgpu').then((m) => m.ModeAA), + [PipelineType.ModeBB]: () => import('anime4k-webgpu').then((m) => m.ModeBB), + [PipelineType.ModeCA]: () => import('anime4k-webgpu').then((m) => m.ModeCA), +}; + +export interface PipelineOption { + pipeline: PipelineType; + params: Record<string, number>; +} + +export class Anime4kContainer extends RenderContainer { + mainTexture: GPUTexture | undefined; + public readonly renderPipeId = 'anime4kRender'; + loadedPipeline: Anime4kPipelineWithOption[] = []; + pipelineChain: Anime4KPipeline[] = []; + + _renderPipeline: GPURenderPipeline | undefined; + _renderBindGroup: GPUBindGroup | undefined; + + constructor(pipelines: Anime4kPipelineWithOption[]) { + super({ + render: (renderer) => { + if (renderer instanceof WebGLRenderer) { + this.glRender(renderer); + } + if (renderer instanceof WebGPURenderer) { + this.gpuRender(renderer, pipelines); + } + }, + }); + } + static loadPipeline( + pipelines: (PipelineType | PipelineOption)[], + ): Promise<Anime4kPipelineConstructor[]> { + return Promise.all( + pipelines.map((pipeline) => { + if (typeof pipeline === 'string') { + return PipelineMap[pipeline]() as Promise<Anime4kPipelineConstructor>; + } + return PipelineMap[ + pipeline.pipeline + ]() as Promise<Anime4kPipelineConstructor>; + }), + ); + } + init( + device: GPUDevice, + firstTexture: GPUTexture, + pipelines: Anime4kPipelineWithOption[], + ) { + this.pipelineChain = pipelines.reduce( + (chain: Anime4KPipeline[], pipeline, index) => { + const inputTexture = + index === 0 ? firstTexture : chain[index - 1].getOutputTexture(); + const pipeClass = pipeline.factory; + const pipeOption = pipeline.params; + const pipe = new pipeClass({ + device, + inputTexture, + nativeDimensions: { + width: firstTexture.width, + height: firstTexture.height, + }, + targetDimensions: { + width: firstTexture.width, + height: firstTexture.height, + }, + }); + if (pipeOption && typeof pipeOption !== 'string') { + for (const [key, value] of Object.entries(pipeOption)) { + pipe.updateParam(key, value); + } + } + chain.push(pipe); + return chain; + }, + [] as Anime4KPipeline[], + ); + const lastPipeline = this.pipelineChain[this.pipelineChain.length - 1]; + if (!lastPipeline) { + return; + } + // render pipeline setups + const renderBindGroupLayout = device.createBindGroupLayout({ + label: 'Render Bind Group Layout', + entries: [ + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + sampler: {}, + }, + { + binding: 2, + visibility: GPUShaderStage.FRAGMENT, + texture: {}, + }, + ], + }); + + const renderPipelineLayout = device.createPipelineLayout({ + label: 'Render Pipeline Layout', + bindGroupLayouts: [renderBindGroupLayout], + }); + + const renderPipeline = device.createRenderPipeline({ + layout: renderPipelineLayout, + vertex: { + module: device.createShaderModule({ + code: fullscreenTexturedQuadVert, + }), + entryPoint: 'vert_main', + }, + fragment: { + module: device.createShaderModule({ + code: sampleExternalTextureFrag, + }), + entryPoint: 'main', + targets: [ + { + format: navigator.gpu.getPreferredCanvasFormat(), + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, + }); + + const sampler = device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', + }); + + const renderBindGroup = device.createBindGroup({ + layout: renderBindGroupLayout, + entries: [ + { + binding: 1, + resource: sampler, + }, + { + binding: 2, + resource: lastPipeline.getOutputTexture().createView(), + }, + ], + }); + this._renderPipeline = renderPipeline; + this._renderBindGroup = renderBindGroup; + } + updatePipeine() { + /* TODO */ + } + glRender(renderer: WebGLRenderer) { + super.render(renderer); + } + gpuRender(renderer: WebGPURenderer, pipelines: Anime4kPipelineWithOption[]) { + super.render(renderer); + + const device = renderer.device.gpu.device; + const colorTexture = renderer.renderTarget.renderTarget.colorTexture; + const gpuRenderTarget = renderer.renderTarget.getGpuRenderTarget( + renderer.renderTarget.renderTarget, + ); + const context = gpuRenderTarget.contexts[0]; + const currentTexture = context.getCurrentTexture(); + + if (!this.mainTexture) { + this.mainTexture = device.createTexture({ + size: [colorTexture.width, colorTexture.height, 1], + format: 'rgba8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + this.init(device, this.mainTexture, pipelines); + } + + if (!context || this.pipelineChain.length === 0) { + console.info('render canceled'); + return; + } + device.queue.copyExternalImageToTexture( + { source: context.canvas }, + { texture: this.mainTexture }, + [colorTexture.width, colorTexture.height], + ); + + const commandEncoder = device.createCommandEncoder(); + for (const pipe of this.pipelineChain) { + pipe.pass(commandEncoder); + } + const passEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: currentTexture.createView(), + clearValue: { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + passEncoder.setPipeline(this._renderPipeline!); + passEncoder.setBindGroup(0, this._renderBindGroup!); + passEncoder.draw(6); + passEncoder.end(); + device.queue.submit([commandEncoder.finish()]); + } +} diff --git a/src/renderer/filter/anime4k/Anime4kRenderPipe.ts b/src/renderer/filter/anime4k/Anime4kRenderPipe.ts new file mode 100644 index 0000000..4b570ac --- /dev/null +++ b/src/renderer/filter/anime4k/Anime4kRenderPipe.ts @@ -0,0 +1,48 @@ +import { + WebGPURenderer, + WebGLRenderer, + extensions, + ExtensionType, + type InstructionSet, + type InstructionPipe, + type Renderer, + type RenderContainer, +} from 'pixi.js'; + +import type { Anime4kContainer } from './Anime4kContainer'; + +export class Anime4kRenderPipe implements InstructionPipe<Anime4kContainer> { + public static extension = { + type: [ExtensionType.WebGPUPipes], + name: 'anime4kRender', + } as const; + + private _renderer: Renderer; + + constructor(renderer: Renderer) { + this._renderer = renderer; + } + + public addRenderable( + container: Anime4kContainer, + instructionSet: InstructionSet, + ): void { + this._renderer.renderPipes.batch.break(instructionSet); + + instructionSet.add(container); + } + + public execute(container: Anime4kContainer) { + if (!container.isRenderable) { + return; + } + container.render(this._renderer); + } + + public destroy(): void { + /* @ts-ignore */ + this._renderer = null; + } +} + +extensions.add(Anime4kRenderPipe); From 6d4253acf1e1dc20b94a5ac5a8653c1686ec18e2 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Mon, 26 Aug 2024 16:33:30 +0800 Subject: [PATCH 05/25] [edit] move action and dye table to tab folder --- src/components/ToolTabPage.tsx | 6 +++--- src/components/{ => tab}/ActionTab/ActionCard.tsx | 0 src/components/{ => tab}/ActionTab/ActionCharacter.tsx | 0 src/components/{ => tab}/ActionTab/ActionTabTitle.tsx | 0 src/components/{ => tab}/ActionTab/ExportAnimateButton.tsx | 0 src/components/{ => tab}/ActionTab/ExportFrameButton.tsx | 0 .../{ => tab}/ActionTab/ExportTypeToggleGroup.tsx | 0 src/components/{ => tab}/ActionTab/helper.ts | 0 src/components/{ => tab}/ActionTab/index.tsx | 0 src/components/{ => tab}/DyeTab/AllColorTable.tsx | 0 src/components/{ => tab}/DyeTab/DyeCharacter.tsx | 0 src/components/{ => tab}/DyeTab/DyeInfo.tsx | 0 src/components/{ => tab}/DyeTab/ExportSeperateButton.tsx | 0 src/components/{ => tab}/DyeTab/ExportTableButton.tsx | 0 src/components/{ => tab}/DyeTab/FaceDyeTab.tsx | 0 src/components/{ => tab}/DyeTab/HairDyeTab.tsx | 0 src/components/{ => tab}/DyeTab/MixDyeTable.tsx | 0 src/components/{ => tab}/DyeTab/styledComponents.ts | 0 18 files changed, 3 insertions(+), 3 deletions(-) rename src/components/{ => tab}/ActionTab/ActionCard.tsx (100%) rename src/components/{ => tab}/ActionTab/ActionCharacter.tsx (100%) rename src/components/{ => tab}/ActionTab/ActionTabTitle.tsx (100%) rename src/components/{ => tab}/ActionTab/ExportAnimateButton.tsx (100%) rename src/components/{ => tab}/ActionTab/ExportFrameButton.tsx (100%) rename src/components/{ => tab}/ActionTab/ExportTypeToggleGroup.tsx (100%) rename src/components/{ => tab}/ActionTab/helper.ts (100%) rename src/components/{ => tab}/ActionTab/index.tsx (100%) rename src/components/{ => tab}/DyeTab/AllColorTable.tsx (100%) rename src/components/{ => tab}/DyeTab/DyeCharacter.tsx (100%) rename src/components/{ => tab}/DyeTab/DyeInfo.tsx (100%) rename src/components/{ => tab}/DyeTab/ExportSeperateButton.tsx (100%) rename src/components/{ => tab}/DyeTab/ExportTableButton.tsx (100%) rename src/components/{ => tab}/DyeTab/FaceDyeTab.tsx (100%) rename src/components/{ => tab}/DyeTab/HairDyeTab.tsx (100%) rename src/components/{ => tab}/DyeTab/MixDyeTable.tsx (100%) rename src/components/{ => tab}/DyeTab/styledComponents.ts (100%) diff --git a/src/components/ToolTabPage.tsx b/src/components/ToolTabPage.tsx index 82be668..1c76d8e 100644 --- a/src/components/ToolTabPage.tsx +++ b/src/components/ToolTabPage.tsx @@ -3,9 +3,9 @@ import { useStore } from '@nanostores/solid'; import { $toolTab } from '@/store/toolTab'; -import { ActionTab } from './ActionTab'; -import { HairDyeTab } from './DyeTab/HairDyeTab'; -import { FaceDyeTab } from './DyeTab/FaceDyeTab'; +import { ActionTab } from './tab/ActionTab'; +import { HairDyeTab } from './tab/DyeTab/HairDyeTab'; +import { FaceDyeTab } from './tab/DyeTab/FaceDyeTab'; import { ToolTab } from '@/const/toolTab'; diff --git a/src/components/ActionTab/ActionCard.tsx b/src/components/tab/ActionTab/ActionCard.tsx similarity index 100% rename from src/components/ActionTab/ActionCard.tsx rename to src/components/tab/ActionTab/ActionCard.tsx diff --git a/src/components/ActionTab/ActionCharacter.tsx b/src/components/tab/ActionTab/ActionCharacter.tsx similarity index 100% rename from src/components/ActionTab/ActionCharacter.tsx rename to src/components/tab/ActionTab/ActionCharacter.tsx diff --git a/src/components/ActionTab/ActionTabTitle.tsx b/src/components/tab/ActionTab/ActionTabTitle.tsx similarity index 100% rename from src/components/ActionTab/ActionTabTitle.tsx rename to src/components/tab/ActionTab/ActionTabTitle.tsx diff --git a/src/components/ActionTab/ExportAnimateButton.tsx b/src/components/tab/ActionTab/ExportAnimateButton.tsx similarity index 100% rename from src/components/ActionTab/ExportAnimateButton.tsx rename to src/components/tab/ActionTab/ExportAnimateButton.tsx diff --git a/src/components/ActionTab/ExportFrameButton.tsx b/src/components/tab/ActionTab/ExportFrameButton.tsx similarity index 100% rename from src/components/ActionTab/ExportFrameButton.tsx rename to src/components/tab/ActionTab/ExportFrameButton.tsx diff --git a/src/components/ActionTab/ExportTypeToggleGroup.tsx b/src/components/tab/ActionTab/ExportTypeToggleGroup.tsx similarity index 100% rename from src/components/ActionTab/ExportTypeToggleGroup.tsx rename to src/components/tab/ActionTab/ExportTypeToggleGroup.tsx diff --git a/src/components/ActionTab/helper.ts b/src/components/tab/ActionTab/helper.ts similarity index 100% rename from src/components/ActionTab/helper.ts rename to src/components/tab/ActionTab/helper.ts diff --git a/src/components/ActionTab/index.tsx b/src/components/tab/ActionTab/index.tsx similarity index 100% rename from src/components/ActionTab/index.tsx rename to src/components/tab/ActionTab/index.tsx diff --git a/src/components/DyeTab/AllColorTable.tsx b/src/components/tab/DyeTab/AllColorTable.tsx similarity index 100% rename from src/components/DyeTab/AllColorTable.tsx rename to src/components/tab/DyeTab/AllColorTable.tsx diff --git a/src/components/DyeTab/DyeCharacter.tsx b/src/components/tab/DyeTab/DyeCharacter.tsx similarity index 100% rename from src/components/DyeTab/DyeCharacter.tsx rename to src/components/tab/DyeTab/DyeCharacter.tsx diff --git a/src/components/DyeTab/DyeInfo.tsx b/src/components/tab/DyeTab/DyeInfo.tsx similarity index 100% rename from src/components/DyeTab/DyeInfo.tsx rename to src/components/tab/DyeTab/DyeInfo.tsx diff --git a/src/components/DyeTab/ExportSeperateButton.tsx b/src/components/tab/DyeTab/ExportSeperateButton.tsx similarity index 100% rename from src/components/DyeTab/ExportSeperateButton.tsx rename to src/components/tab/DyeTab/ExportSeperateButton.tsx diff --git a/src/components/DyeTab/ExportTableButton.tsx b/src/components/tab/DyeTab/ExportTableButton.tsx similarity index 100% rename from src/components/DyeTab/ExportTableButton.tsx rename to src/components/tab/DyeTab/ExportTableButton.tsx diff --git a/src/components/DyeTab/FaceDyeTab.tsx b/src/components/tab/DyeTab/FaceDyeTab.tsx similarity index 100% rename from src/components/DyeTab/FaceDyeTab.tsx rename to src/components/tab/DyeTab/FaceDyeTab.tsx diff --git a/src/components/DyeTab/HairDyeTab.tsx b/src/components/tab/DyeTab/HairDyeTab.tsx similarity index 100% rename from src/components/DyeTab/HairDyeTab.tsx rename to src/components/tab/DyeTab/HairDyeTab.tsx diff --git a/src/components/DyeTab/MixDyeTable.tsx b/src/components/tab/DyeTab/MixDyeTable.tsx similarity index 100% rename from src/components/DyeTab/MixDyeTable.tsx rename to src/components/tab/DyeTab/MixDyeTable.tsx diff --git a/src/components/DyeTab/styledComponents.ts b/src/components/tab/DyeTab/styledComponents.ts similarity index 100% rename from src/components/DyeTab/styledComponents.ts rename to src/components/tab/DyeTab/styledComponents.ts From f1ad015f6b29ed463ad352dbf1c2189e978b96de Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Tue, 27 Aug 2024 19:08:05 +0800 Subject: [PATCH 06/25] [add] Anime4k System and Filter and successfully apply on pixi application --- src/components/CharacterPreview/Character.tsx | 33 ++ src/lucide-solid.d.ts | 2 + .../filter/anime4k/Anime4kContainer.ts | 321 ------------------ src/renderer/filter/anime4k/Anime4kFilter.ts | 273 +++++++++++++++ .../filter/anime4k/Anime4kRenderPipe.ts | 48 --- .../filter/anime4k/Anime4kSyetem.d.ts | 9 + src/renderer/filter/anime4k/Anime4kSystem.ts | 272 +++++++++++++++ src/renderer/filter/anime4k/const.ts | 110 ++++++ src/renderer/filter/anime4k/index.ts | 4 +- .../filter/anime4k/sampleExternalTexture.wgsl | 6 +- 10 files changed, 705 insertions(+), 373 deletions(-) delete mode 100644 src/renderer/filter/anime4k/Anime4kContainer.ts create mode 100644 src/renderer/filter/anime4k/Anime4kFilter.ts delete mode 100644 src/renderer/filter/anime4k/Anime4kRenderPipe.ts create mode 100644 src/renderer/filter/anime4k/Anime4kSyetem.d.ts create mode 100644 src/renderer/filter/anime4k/Anime4kSystem.ts create mode 100644 src/renderer/filter/anime4k/const.ts diff --git a/src/components/CharacterPreview/Character.tsx b/src/components/CharacterPreview/Character.tsx index 448837a..b95b56f 100644 --- a/src/components/CharacterPreview/Character.tsx +++ b/src/components/CharacterPreview/Character.tsx @@ -15,12 +15,20 @@ import { resetUpscaleSource, setUpscaleSource, } from '@/store/expirement/upscale'; +import { $showUpscaledCharacter } from '@/store/trigger'; import { usePureStore } from '@/store'; import { Application } from 'pixi.js'; import { Character } from '@/renderer/character/character'; import { ZoomContainer } from '@/renderer/ZoomContainer'; +/* TEST */ +import { Anime4kFilter } from '@/renderer/filter/anime4k/Anime4kFilter'; +import { + PipelineType, + type PipelineOption, +} from '@/renderer/filter/anime4k/const'; + export interface CharacterViewProps { onLoad: () => void; onLoaded: () => void; @@ -32,8 +40,10 @@ export const CharacterView = (props: CharacterViewProps) => { const zoomInfo = usePureStore($previewZoomInfo); const characterData = usePureStore(props.store); const [isInit, setIsInit] = createSignal<boolean>(false); + const isShowUpscale = usePureStore($showUpscaledCharacter); let container!: HTMLDivElement; let viewport: ZoomContainer | undefined = undefined; + let upscaleFilter: Anime4kFilter | undefined = undefined; const app = new Application(); const ch = new Character(app); @@ -79,6 +89,7 @@ export const CharacterView = (props: CharacterViewProps) => { }); container.appendChild(app.canvas); viewport.addChild(ch); + app.stage.addChild(viewport); if (props.target === 'preview') { setUpscaleSource(app.canvas); @@ -116,6 +127,28 @@ export const CharacterView = (props: CharacterViewProps) => { } }); + createEffect(async () => { + if (isShowUpscale()) { + await app.renderer.anime4k.preparePipeline([PipelineType.ModeBB]); + if (!upscaleFilter) { + upscaleFilter = new Anime4kFilter([ + { + pipeline: PipelineType.ModeBB, + // params: { + // strength: 0.5, + // strength2: 1, + // }, + } as PipelineOption, + ]); + } + /* TODO */ + upscaleFilter.updatePipeine(); + app.stage.filters = [upscaleFilter]; + } else { + app.stage.filters = []; + } + }); + createEffect(() => { const info = zoomInfo(); const scaled = info.zoom; diff --git a/src/lucide-solid.d.ts b/src/lucide-solid.d.ts index b31c157..8fd8e8b 100644 --- a/src/lucide-solid.d.ts +++ b/src/lucide-solid.d.ts @@ -10,3 +10,5 @@ declare module 'dom-to-image-more' { import domToImage = require('dom-to-image'); export = domToImage; } + +/// <reference path="./renderer/filter/anime4k/Anime4kSyetem.d.ts" /> diff --git a/src/renderer/filter/anime4k/Anime4kContainer.ts b/src/renderer/filter/anime4k/Anime4kContainer.ts deleted file mode 100644 index b0493a7..0000000 --- a/src/renderer/filter/anime4k/Anime4kContainer.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { RenderContainer, WebGPURenderer, WebGLRenderer } from 'pixi.js'; - -import type { Anime4KPipeline } from 'anime4k-webgpu'; -import fullscreenTexturedQuadVert from './fullscreenTexturedQuad.wgsl'; -import sampleExternalTextureFrag from './sampleExternalTexture.wgsl'; - -type Anime4kPipelineConstructor = new (desc: { - device: GPUDevice; - inputTexture: GPUTexture; - nativeDimensions?: { width: number; height: number }; - targetDimensions?: { width: number; height: number }; -}) => Anime4KPipeline; -export interface Anime4kPipelineWithOption { - factory: Anime4kPipelineConstructor; - params?: Record<string, number>; -} - -export enum PipelineType { - /** - * deblur, must set param: strength - * - * @param strength Deblur Strength (0.1 - 15.0) - */ - Dog = 'Dog', - /** - * denoise, must set param: strength, strength2 - * - * @param strength Itensity Sigma (0.1 - 2.0) - * @param strength2 Spatial Sigma (1 - 15) - */ - BilateralMean = 'BilateralMean', - - /** restore */ - CNNM = 'CNNM', - /** restore */ - CNNSoftM = 'CNNSoftM', - /** restore */ - CNNSoftVL = 'CNNSoftVL', - /** restore */ - CNNVL = 'CNNVL', - /** restore */ - CNNUL = 'CNNUL', - /** restore */ - GANUUL = 'GANUUL', - - /** upscale */ - CNNx2M = 'CNNx2M', - /** upscale */ - CNNx2VL = 'CNNx2VL', - /** upscale */ - DenoiseCNNx2VL = 'DenoiseCNNx2VL', - /** upscale */ - CNNx2UL = 'CNNx2UL', - /** upscale */ - GANx3L = 'GANx3L', - /** upscale */ - GANx4UUL = 'GANx4UUL', - - ClampHighlights = 'ClampHighlights', - DepthToSpace = 'DepthToSpace', - - /* anime4k preset @see https://github.com/bloc97/Anime4K/blob/master/md/GLSL_Instructions_Advanced.md */ - /** Restore -> Upscale -> Upscale */ - ModeA = 'ModeA', - /** Restore_Soft -> Upscale -> Upscale */ - ModeB = 'ModeB', - /** Upscale_Denoise -> Upscale */ - ModeC = 'ModeC', - /** Restore -> Upscale -> Restore -> Upscale */ - ModeAA = 'ModeAA', - /** Restore_Soft -> Upscale -> Restore_Soft -> Upscale */ - ModeBB = 'ModeBB', - /** Upscale_Denoise -> Restore -> Upscale */ - ModeCA = 'ModeCA', -} - -const PipelineMap: Record<PipelineType, () => Promise<unknown>> = { - [PipelineType.Dog]: () => import('anime4k-webgpu').then((m) => m.DoG), - [PipelineType.BilateralMean]: () => - import('anime4k-webgpu').then((m) => m.BilateralMean), - - [PipelineType.CNNM]: () => import('anime4k-webgpu').then((m) => m.CNNM), - [PipelineType.CNNSoftM]: () => - import('anime4k-webgpu').then((m) => m.CNNSoftM), - [PipelineType.CNNSoftVL]: () => - import('anime4k-webgpu').then((m) => m.CNNSoftVL), - [PipelineType.CNNVL]: () => import('anime4k-webgpu').then((m) => m.CNNVL), - [PipelineType.CNNUL]: () => import('anime4k-webgpu').then((m) => m.CNNUL), - [PipelineType.GANUUL]: () => import('anime4k-webgpu').then((m) => m.GANUUL), - - [PipelineType.CNNx2M]: () => import('anime4k-webgpu').then((m) => m.CNNx2M), - [PipelineType.CNNx2VL]: () => import('anime4k-webgpu').then((m) => m.CNNx2VL), - [PipelineType.DenoiseCNNx2VL]: () => - import('anime4k-webgpu').then((m) => m.DenoiseCNNx2VL), - [PipelineType.CNNx2UL]: () => import('anime4k-webgpu').then((m) => m.CNNx2UL), - [PipelineType.GANx3L]: () => import('anime4k-webgpu').then((m) => m.GANx3L), - [PipelineType.GANx4UUL]: () => - import('anime4k-webgpu').then((m) => m.GANx4UUL), - - [PipelineType.ClampHighlights]: () => - import('anime4k-webgpu').then((m) => m.ClampHighlights), - [PipelineType.DepthToSpace]: () => - import('anime4k-webgpu').then((m) => m.DepthToSpace), - - [PipelineType.ModeA]: () => import('anime4k-webgpu').then((m) => m.ModeA), - [PipelineType.ModeB]: () => import('anime4k-webgpu').then((m) => m.ModeB), - [PipelineType.ModeC]: () => import('anime4k-webgpu').then((m) => m.ModeC), - [PipelineType.ModeAA]: () => import('anime4k-webgpu').then((m) => m.ModeAA), - [PipelineType.ModeBB]: () => import('anime4k-webgpu').then((m) => m.ModeBB), - [PipelineType.ModeCA]: () => import('anime4k-webgpu').then((m) => m.ModeCA), -}; - -export interface PipelineOption { - pipeline: PipelineType; - params: Record<string, number>; -} - -export class Anime4kContainer extends RenderContainer { - mainTexture: GPUTexture | undefined; - public readonly renderPipeId = 'anime4kRender'; - loadedPipeline: Anime4kPipelineWithOption[] = []; - pipelineChain: Anime4KPipeline[] = []; - - _renderPipeline: GPURenderPipeline | undefined; - _renderBindGroup: GPUBindGroup | undefined; - - constructor(pipelines: Anime4kPipelineWithOption[]) { - super({ - render: (renderer) => { - if (renderer instanceof WebGLRenderer) { - this.glRender(renderer); - } - if (renderer instanceof WebGPURenderer) { - this.gpuRender(renderer, pipelines); - } - }, - }); - } - static loadPipeline( - pipelines: (PipelineType | PipelineOption)[], - ): Promise<Anime4kPipelineConstructor[]> { - return Promise.all( - pipelines.map((pipeline) => { - if (typeof pipeline === 'string') { - return PipelineMap[pipeline]() as Promise<Anime4kPipelineConstructor>; - } - return PipelineMap[ - pipeline.pipeline - ]() as Promise<Anime4kPipelineConstructor>; - }), - ); - } - init( - device: GPUDevice, - firstTexture: GPUTexture, - pipelines: Anime4kPipelineWithOption[], - ) { - this.pipelineChain = pipelines.reduce( - (chain: Anime4KPipeline[], pipeline, index) => { - const inputTexture = - index === 0 ? firstTexture : chain[index - 1].getOutputTexture(); - const pipeClass = pipeline.factory; - const pipeOption = pipeline.params; - const pipe = new pipeClass({ - device, - inputTexture, - nativeDimensions: { - width: firstTexture.width, - height: firstTexture.height, - }, - targetDimensions: { - width: firstTexture.width, - height: firstTexture.height, - }, - }); - if (pipeOption && typeof pipeOption !== 'string') { - for (const [key, value] of Object.entries(pipeOption)) { - pipe.updateParam(key, value); - } - } - chain.push(pipe); - return chain; - }, - [] as Anime4KPipeline[], - ); - const lastPipeline = this.pipelineChain[this.pipelineChain.length - 1]; - if (!lastPipeline) { - return; - } - // render pipeline setups - const renderBindGroupLayout = device.createBindGroupLayout({ - label: 'Render Bind Group Layout', - entries: [ - { - binding: 1, - visibility: GPUShaderStage.FRAGMENT, - sampler: {}, - }, - { - binding: 2, - visibility: GPUShaderStage.FRAGMENT, - texture: {}, - }, - ], - }); - - const renderPipelineLayout = device.createPipelineLayout({ - label: 'Render Pipeline Layout', - bindGroupLayouts: [renderBindGroupLayout], - }); - - const renderPipeline = device.createRenderPipeline({ - layout: renderPipelineLayout, - vertex: { - module: device.createShaderModule({ - code: fullscreenTexturedQuadVert, - }), - entryPoint: 'vert_main', - }, - fragment: { - module: device.createShaderModule({ - code: sampleExternalTextureFrag, - }), - entryPoint: 'main', - targets: [ - { - format: navigator.gpu.getPreferredCanvasFormat(), - }, - ], - }, - primitive: { - topology: 'triangle-list', - }, - }); - - const sampler = device.createSampler({ - magFilter: 'linear', - minFilter: 'linear', - }); - - const renderBindGroup = device.createBindGroup({ - layout: renderBindGroupLayout, - entries: [ - { - binding: 1, - resource: sampler, - }, - { - binding: 2, - resource: lastPipeline.getOutputTexture().createView(), - }, - ], - }); - this._renderPipeline = renderPipeline; - this._renderBindGroup = renderBindGroup; - } - updatePipeine() { - /* TODO */ - } - glRender(renderer: WebGLRenderer) { - super.render(renderer); - } - gpuRender(renderer: WebGPURenderer, pipelines: Anime4kPipelineWithOption[]) { - super.render(renderer); - - const device = renderer.device.gpu.device; - const colorTexture = renderer.renderTarget.renderTarget.colorTexture; - const gpuRenderTarget = renderer.renderTarget.getGpuRenderTarget( - renderer.renderTarget.renderTarget, - ); - const context = gpuRenderTarget.contexts[0]; - const currentTexture = context.getCurrentTexture(); - - if (!this.mainTexture) { - this.mainTexture = device.createTexture({ - size: [colorTexture.width, colorTexture.height, 1], - format: 'rgba8unorm', - usage: - GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT, - }); - this.init(device, this.mainTexture, pipelines); - } - - if (!context || this.pipelineChain.length === 0) { - console.info('render canceled'); - return; - } - device.queue.copyExternalImageToTexture( - { source: context.canvas }, - { texture: this.mainTexture }, - [colorTexture.width, colorTexture.height], - ); - - const commandEncoder = device.createCommandEncoder(); - for (const pipe of this.pipelineChain) { - pipe.pass(commandEncoder); - } - const passEncoder = commandEncoder.beginRenderPass({ - colorAttachments: [ - { - view: currentTexture.createView(), - clearValue: { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }, - loadOp: 'clear', - storeOp: 'store', - }, - ], - }); - passEncoder.setPipeline(this._renderPipeline!); - passEncoder.setBindGroup(0, this._renderBindGroup!); - passEncoder.draw(6); - passEncoder.end(); - device.queue.submit([commandEncoder.finish()]); - } -} diff --git a/src/renderer/filter/anime4k/Anime4kFilter.ts b/src/renderer/filter/anime4k/Anime4kFilter.ts new file mode 100644 index 0000000..e4acd91 --- /dev/null +++ b/src/renderer/filter/anime4k/Anime4kFilter.ts @@ -0,0 +1,273 @@ +import { + Filter, + Texture, + GpuProgram, + Geometry, + Point, + type WebGPURenderer, + type FilterSystem, + type RenderSurface, +} from 'pixi.js'; +import { wgslVertex } from 'pixi-filters'; +import './Anime4kSystem'; + +import sampleExternalTextureFrag from './sampleExternalTexture.wgsl'; + +import { type PipelineOption, PipelineType } from './const'; + +const quadGeometry = new Geometry({ + attributes: { + aPosition: { + buffer: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]), + format: 'float32x2', + stride: 2 * 4, + offset: 0, + }, + }, + indexBuffer: new Uint32Array([0, 1, 2, 0, 2, 3]), +}); + +export class Anime4kFilter extends Filter { + mainTexture: GPUTexture | undefined; + _texture: GPUTexture | undefined; + public readonly renderPipeId = 'anime4kRender'; + loadedPipeline: PipelineOption[] = []; + + constructor(pipelines: PipelineOption[]) { + const gpuProgram = GpuProgram.from({ + vertex: { + source: wgslVertex, + entryPoint: 'mainVertex', + }, + fragment: { + source: sampleExternalTextureFrag, + entryPoint: 'main', + }, + }); + super({ + gpuProgram, + resources: {}, + }); + this.loadedPipeline = pipelines; + } + public override apply( + filterManager: FilterSystem, + input: Texture, + output: RenderSurface, + clearMode: boolean, + ) { + const renderer = filterManager.renderer as WebGPURenderer; + const globalBindGroup = this.getPixiGlobalBindGroup( + filterManager, + input, + output, + ); + this.groups[0] = globalBindGroup; + + /* FilterSystem done */ + + const res = (filterManager.renderer as WebGPURenderer).texture.getGpuSource( + /* @ts-ignore */ + input.source, + ); + + const sizeOption = { + width: input.source.width, + height: input.source.height, + }; + const resource = renderer.anime4k.getSizedRenderResource( + sizeOption, + this.loadedPipeline, + ); + + /* copy incoming texture to mainTexture so anime4k can process it */ + renderer.encoder.commandEncoder.copyTextureToTexture( + { + texture: res, + }, + { + texture: resource[0], + }, + [input.source.width, input.source.height, 1], + ); + + for (const pipe of resource[2]) { + pipe.pass(renderer.encoder.commandEncoder); + } + + renderer.renderTarget.bind(output, !!clearMode); + + renderer.encoder.setPipelineFromGeometryProgramAndState( + quadGeometry, + this.gpuProgram, + this._state, + 'triangle-list', + ); + renderer.encoder.setGeometry(quadGeometry, this.gpuProgram); + + /* @ts-ignore */ + renderer.encoder._syncBindGroup(globalBindGroup); + /* @ts-ignore */ + renderer.encoder._boundBindGroup[0] = globalBindGroup; + + globalBindGroup._touch(renderer.textureGC.count); + + /* create a bindGroup and use last pipeline result as texture */ + const bindGroup = renderer.anime4k.getSizedBindGroup(sizeOption, { + bindGroup: globalBindGroup, + program: this.gpuProgram, + groupIndex: 0, + resourceView: resource[1], + }); + + renderer.encoder.setPipelineFromGeometryProgramAndState( + quadGeometry, + this.gpuProgram, + this._state, + 'triangle-list', + ); + renderer.encoder.setGeometry(quadGeometry, this.gpuProgram); + renderer.encoder.renderPassEncoder.setBindGroup(0, bindGroup); + renderer.encoder.renderPassEncoder.drawIndexed( + quadGeometry.indexBuffer.data.length, + quadGeometry.instanceCount, + 0, + ); + } + public updatePipeine() { + /* TODO */ + } + private getPixiGlobalBindGroup( + filterManager: FilterSystem, + input: Texture, + output: RenderSurface, + ) { + const renderer = filterManager.renderer as WebGPURenderer; + const renderTarget = renderer.renderTarget.getRenderTarget(output); + const rootTexture = renderer.renderTarget.rootRenderTarget.colorTexture; + /* FilterSystem stuff start, the code is just copy paste from pixi.js v8 src/filters/FilterSystem */ + /* @ts-ignore */ + const filterData = + /* @ts-ignore */ + filterManager._filterStack[filterManager._filterStackIndex]; + const bounds = filterData.bounds; + + const offset = Point.shared; + + const previousRenderSurface = filterData.previousRenderSurface; + + const isFinalTarget = previousRenderSurface === output; + + let resolution = rootTexture.source._resolution; + + // to find the previous resolution we need to account for the skipped filters + // the following will find the last non skipped filter... + /* @ts-ignore */ + let currentIndex = this._filterStackIndex - 1; + + /* @ts-ignore */ + while (currentIndex > 0 && this._filterStack[currentIndex].skip) { + --currentIndex; + } + + if (currentIndex > 0) { + resolution = + /* @ts-ignore */ + this._filterStack[currentIndex].inputTexture.source._resolution; + } + + /* it private it also necessary access that here */ + /* @ts-ignore */ + const filterUniforms = filterManager._filterGlobalUniforms; + const uniforms = filterUniforms.uniforms; + + const outputFrame = uniforms.uOutputFrame; + const inputSize = uniforms.uInputSize; + const inputPixel = uniforms.uInputPixel; + const inputClamp = uniforms.uInputClamp; + const globalFrame = uniforms.uGlobalFrame; + const outputTexture = uniforms.uOutputTexture; + + // are we rendering back to the original surface? + if (isFinalTarget) { + /* @ts-ignore */ + let lastIndex = filterManager._filterStackIndex; + + // get previous bounds.. we must take into account skipped filters also.. + while (lastIndex > 0) { + lastIndex--; + const filterData = + /* @ts-ignore */ + filterManager._filterStack[filterManager._filterStackIndex - 1]; + + if (!filterData.skip) { + offset.x = filterData.bounds.minX; + offset.y = filterData.bounds.minY; + + break; + } + } + + outputFrame[0] = bounds.minX - offset.x; + outputFrame[1] = bounds.minY - offset.y; + } else { + outputFrame[0] = 0; + outputFrame[1] = 0; + } + + outputFrame[2] = input.frame.width; + outputFrame[3] = input.frame.height; + + inputSize[0] = input.source.width; + inputSize[1] = input.source.height; + inputSize[2] = 1 / inputSize[0]; + inputSize[3] = 1 / inputSize[1]; + + inputPixel[0] = input.source.pixelWidth; + inputPixel[1] = input.source.pixelHeight; + inputPixel[2] = 1.0 / inputPixel[0]; + inputPixel[3] = 1.0 / inputPixel[1]; + + inputClamp[0] = 0.5 * inputPixel[2]; + inputClamp[1] = 0.5 * inputPixel[3]; + inputClamp[2] = input.frame.width * inputSize[2] - 0.5 * inputPixel[2]; + inputClamp[3] = input.frame.height * inputSize[3] - 0.5 * inputPixel[3]; + + globalFrame[0] = offset.x * resolution; + globalFrame[1] = offset.y * resolution; + + globalFrame[2] = rootTexture.source.width * resolution; + globalFrame[3] = rootTexture.source.height * resolution; + + if (output instanceof Texture) { + outputTexture[0] = output.frame.width; + outputTexture[1] = output.frame.height; + } else { + // this means a renderTarget was passed directly + outputTexture[0] = renderTarget.width; + outputTexture[1] = renderTarget.height; + } + + outputTexture[2] = renderTarget.isRoot ? -1 : 1; + filterUniforms.update(); + + /* @ts-ignore */ + const globalBindGroup = filterManager._globalFilterBindGroup; + if (renderer.renderPipes.uniformBatch) { + const batchUniforms = + renderer.renderPipes.uniformBatch.getUboResource(filterUniforms); + + globalBindGroup.setResource(batchUniforms, 0); + } else { + globalBindGroup.setResource(filterUniforms, 0); + } + + // now lets update the output texture... + + // set bind group.. + globalBindGroup.setResource(input.source, 1); + globalBindGroup.setResource(input.source.style, 2); + + return globalBindGroup; + } +} diff --git a/src/renderer/filter/anime4k/Anime4kRenderPipe.ts b/src/renderer/filter/anime4k/Anime4kRenderPipe.ts deleted file mode 100644 index 4b570ac..0000000 --- a/src/renderer/filter/anime4k/Anime4kRenderPipe.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - WebGPURenderer, - WebGLRenderer, - extensions, - ExtensionType, - type InstructionSet, - type InstructionPipe, - type Renderer, - type RenderContainer, -} from 'pixi.js'; - -import type { Anime4kContainer } from './Anime4kContainer'; - -export class Anime4kRenderPipe implements InstructionPipe<Anime4kContainer> { - public static extension = { - type: [ExtensionType.WebGPUPipes], - name: 'anime4kRender', - } as const; - - private _renderer: Renderer; - - constructor(renderer: Renderer) { - this._renderer = renderer; - } - - public addRenderable( - container: Anime4kContainer, - instructionSet: InstructionSet, - ): void { - this._renderer.renderPipes.batch.break(instructionSet); - - instructionSet.add(container); - } - - public execute(container: Anime4kContainer) { - if (!container.isRenderable) { - return; - } - container.render(this._renderer); - } - - public destroy(): void { - /* @ts-ignore */ - this._renderer = null; - } -} - -extensions.add(Anime4kRenderPipe); diff --git a/src/renderer/filter/anime4k/Anime4kSyetem.d.ts b/src/renderer/filter/anime4k/Anime4kSyetem.d.ts new file mode 100644 index 0000000..deb630d --- /dev/null +++ b/src/renderer/filter/anime4k/Anime4kSyetem.d.ts @@ -0,0 +1,9 @@ +declare global { + namespace PixiMixins { + interface RendererSystems { + anime4k: import('./Anime4kSystem').Anime4kFilterSystem; + } + } +} + +export {}; diff --git a/src/renderer/filter/anime4k/Anime4kSystem.ts b/src/renderer/filter/anime4k/Anime4kSystem.ts new file mode 100644 index 0000000..c45a8ac --- /dev/null +++ b/src/renderer/filter/anime4k/Anime4kSystem.ts @@ -0,0 +1,272 @@ +import { + extensions, + ExtensionType, + type WebGPURenderer, + type Renderer, + type System, + type GpuProgram, + type BindGroup, + type UniformGroup, + type TextureStyle, + type BufferResource, + type Buffer, + type BindResource, + type GPU, +} from 'pixi.js'; +import type { Anime4KPipeline } from 'anime4k-webgpu'; +import { + PipelineMap, + type PipelineType, + type PipelineOption, + type Anime4kPipelineConstructor, +} from './const'; + +export type SizedRenderResourceTuple = [ + GPUTexture, + GPUTextureView, + Anime4KPipeline[], +]; + +export class Anime4kFilterSystem implements System { + public static extension = { + type: [ExtensionType.WebGPUSystem], + name: 'anime4k', + } as const; + + private _renderer: Renderer; + private _pipelineMap: Map<PipelineType, Anime4kPipelineConstructor> = + new Map(); + private _sizedPipelineMap: Map<string, Map<string, Anime4KPipeline>> = + new Map(); + private _sizedRenderMap: Map<string, SizedRenderResourceTuple> = new Map(); + private _sizedBindGroupMap: Map<string, GPUBindGroup> = new Map(); + private _gpu!: GPU; + + constructor(renderer: Renderer) { + this._renderer = renderer; + } + + protected contextChange(gpu: GPU): void { + this._gpu = gpu; + } + + public destroy(): void { + /* @ts-ignore */ + this._renderer = null; + } + public clear(): void { + this._sizedRenderMap.clear(); + this._sizedBindGroupMap.clear(); + this._sizedPipelineMap.clear(); + } + + public preparePipeline(pipelines: PipelineType[]) { + const unresolved = pipelines.filter((p) => !this._pipelineMap.has(p)); + return Promise.all( + unresolved.map((p) => + PipelineMap[p]().then((m) => + this._pipelineMap.set(p, m as Anime4kPipelineConstructor), + ), + ), + ); + } + + private getSizedPipeline( + option: { width: number; height: number; inputTexture: GPUTexture }, + type: PipelineType, + index: number, + ) { + const key = `${option.width}x${option.height}`; + let sizedMap = this._sizedPipelineMap.get(key); + if (!sizedMap) { + sizedMap = new Map(); + this._sizedPipelineMap.set(key, sizedMap); + } + + const pipelineClass = this._pipelineMap.get(type); + if (!pipelineClass) { + throw new Error(`Pipeline ${type} not prepared or not found`); + } + + const pipelineKey = `${type}:${index}`; + let targetPipeline = sizedMap.get(pipelineKey); + + if (!targetPipeline) { + targetPipeline = new pipelineClass({ + device: this._gpu.device, + inputTexture: option.inputTexture, + nativeDimensions: { + width: option.width, + height: option.height, + }, + targetDimensions: { + width: option.width, + height: option.height, + }, + }); + sizedMap.set(pipelineKey, targetPipeline); + } + + return targetPipeline; + } + getSizedRenderResource( + option: { + width: number; + height: number; + }, + pipelines: PipelineOption[], + ) { + const pipelineHash = pipelines.map((p) => p.pipeline).join(','); + const key = `${option.width}x${option.height}:${pipelineHash}`; + let resource = this._sizedRenderMap.get(key); + if (!resource) { + resource = this.setSizedRenderResource(option, pipelines); + this._sizedRenderMap.set(key, resource); + } + + return resource; + } + setSizedRenderResource( + option: { + width: number; + height: number; + }, + pipelines: PipelineOption[], + ): SizedRenderResourceTuple { + const device = this._gpu.device; + const mainTexture = device.createTexture({ + size: [option.width, option.height, 1], + format: 'bgra8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.COPY_SRC | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const pipelineChain = pipelines.reduce( + (chain: Anime4KPipeline[], pipeline, index) => { + const inputTexture = + index === 0 ? mainTexture : chain[index - 1].getOutputTexture(); + const pipe = this.getSizedPipeline( + { + width: option.width, + height: option.height, + inputTexture, + }, + pipeline.pipeline, + index, + ); + /* notice that just direct update, means all thing using this will also got update */ + if (pipeline.params) { + for (const [key, value] of Object.entries(pipeline.params)) { + pipe.updateParam(key, value); + } + } + chain.push(pipe); + return chain; + }, + [] as Anime4KPipeline[], + ); + const lastPipeline = pipelineChain[pipelineChain.length - 1]; + return [ + mainTexture, + lastPipeline.getOutputTexture().createView(), + pipelineChain, + ]; + } + getSizedBindGroup( + option: { width: number; height: number }, + groupOption: { + bindGroup: BindGroup; + program: GpuProgram; + groupIndex: number; + resourceView: GPUTextureView; + }, + ) { + const key = `${option.width}x${option.height}`; + let bindGroup = this._sizedBindGroupMap.get(key); + if (!bindGroup) { + bindGroup = this.setSizedBindGroup(groupOption); + this._sizedBindGroupMap.set(key, bindGroup); + } + return bindGroup; + } + setSizedBindGroup(groupOption: { + bindGroup: BindGroup; + program: GpuProgram; + groupIndex: number; + resourceView: GPUTextureView; + }) { + const device = this._gpu.device; + const renderer = this._renderer as WebGPURenderer; + const { bindGroup, program, groupIndex, resourceView } = groupOption; + const groupLayout = program.layout[groupIndex]; + const entries: GPUBindGroupEntry[] = []; + + for (const j in groupLayout) { + const resource: BindResource = + bindGroup.resources[j] ?? bindGroup.resources[groupLayout[j]]; + let gpuResource!: + | GPUSampler + | GPUTextureView + | GPUExternalTexture + | GPUBufferBinding; + // TODO make this dynamic.. + + if (resource._resourceType === 'uniformGroup') { + const uniformGroup = resource as UniformGroup; + + renderer.ubo.updateUniformGroup(uniformGroup as UniformGroup); + + const buffer = uniformGroup.buffer as Buffer; + + gpuResource = { + buffer: renderer.buffer.getGPUBuffer(buffer), + offset: 0, + size: buffer.descriptor.size, + }; + } else if (resource._resourceType === 'buffer') { + const buffer = resource as Buffer; + + gpuResource = { + buffer: renderer.buffer.getGPUBuffer(buffer), + offset: 0, + size: buffer.descriptor.size, + }; + } else if (resource._resourceType === 'bufferResource') { + const bufferResource = resource as BufferResource; + + gpuResource = { + buffer: renderer.buffer.getGPUBuffer(bufferResource.buffer), + offset: bufferResource.offset, + size: bufferResource.size, + }; + } else if (resource._resourceType === 'textureSampler') { + const sampler = resource as TextureStyle; + + gpuResource = renderer.texture.getGpuSampler(sampler); + } else if (resource._resourceType === 'textureSource') { + gpuResource = resourceView; + } + + if (gpuResource) { + entries.push({ + binding: groupLayout[j], + resource: gpuResource, + }); + } + } + + const layout = + renderer.shader.getProgramData(program).bindGroups[groupIndex]; + const gpuBindGroup = device.createBindGroup({ + layout, + entries, + }); + + return gpuBindGroup; + } +} + +extensions.add(Anime4kFilterSystem); diff --git a/src/renderer/filter/anime4k/const.ts b/src/renderer/filter/anime4k/const.ts new file mode 100644 index 0000000..f450400 --- /dev/null +++ b/src/renderer/filter/anime4k/const.ts @@ -0,0 +1,110 @@ +import type { Anime4KPipeline } from 'anime4k-webgpu'; + +export enum PipelineType { + /** + * deblur, must set param: strength + * + * @param strength Deblur Strength (0.1 - 15.0) + */ + Dog = 'Dog', + /** + * denoise, must set param: strength, strength2 + * + * @param strength Itensity Sigma (0.1 - 2.0) + * @param strength2 Spatial Sigma (1 - 15) + */ + BilateralMean = 'BilateralMean', + + /** restore */ + CNNM = 'CNNM', + /** restore */ + CNNSoftM = 'CNNSoftM', + /** restore */ + CNNSoftVL = 'CNNSoftVL', + /** restore */ + CNNVL = 'CNNVL', + /** restore */ + CNNUL = 'CNNUL', + /** restore */ + GANUUL = 'GANUUL', + + /** upscale */ + CNNx2M = 'CNNx2M', + /** upscale */ + CNNx2VL = 'CNNx2VL', + /** upscale */ + DenoiseCNNx2VL = 'DenoiseCNNx2VL', + /** upscale */ + CNNx2UL = 'CNNx2UL', + /** upscale */ + GANx3L = 'GANx3L', + /** upscale */ + GANx4UUL = 'GANx4UUL', + + ClampHighlights = 'ClampHighlights', + DepthToSpace = 'DepthToSpace', + + /* anime4k preset @see https://github.com/bloc97/Anime4K/blob/master/md/GLSL_Instructions_Advanced.md */ + /** Restore -> Upscale -> Upscale */ + ModeA = 'ModeA', + /** Restore_Soft -> Upscale -> Upscale */ + ModeB = 'ModeB', + /** Upscale_Denoise -> Upscale */ + ModeC = 'ModeC', + /** Restore -> Upscale -> Restore -> Upscale */ + ModeAA = 'ModeAA', + /** Restore_Soft -> Upscale -> Restore_Soft -> Upscale */ + ModeBB = 'ModeBB', + /** Upscale_Denoise -> Restore -> Upscale */ + ModeCA = 'ModeCA', +} + +export const PipelineMap: Record<PipelineType, () => Promise<unknown>> = { + [PipelineType.Dog]: () => import('anime4k-webgpu').then((m) => m.DoG), + [PipelineType.BilateralMean]: () => + import('anime4k-webgpu').then((m) => m.BilateralMean), + + [PipelineType.CNNM]: () => import('anime4k-webgpu').then((m) => m.CNNM), + [PipelineType.CNNSoftM]: () => + import('anime4k-webgpu').then((m) => m.CNNSoftM), + [PipelineType.CNNSoftVL]: () => + import('anime4k-webgpu').then((m) => m.CNNSoftVL), + [PipelineType.CNNVL]: () => import('anime4k-webgpu').then((m) => m.CNNVL), + [PipelineType.CNNUL]: () => import('anime4k-webgpu').then((m) => m.CNNUL), + [PipelineType.GANUUL]: () => import('anime4k-webgpu').then((m) => m.GANUUL), + + [PipelineType.CNNx2M]: () => import('anime4k-webgpu').then((m) => m.CNNx2M), + [PipelineType.CNNx2VL]: () => import('anime4k-webgpu').then((m) => m.CNNx2VL), + [PipelineType.DenoiseCNNx2VL]: () => + import('anime4k-webgpu').then((m) => m.DenoiseCNNx2VL), + [PipelineType.CNNx2UL]: () => import('anime4k-webgpu').then((m) => m.CNNx2UL), + [PipelineType.GANx3L]: () => import('anime4k-webgpu').then((m) => m.GANx3L), + [PipelineType.GANx4UUL]: () => + import('anime4k-webgpu').then((m) => m.GANx4UUL), + + [PipelineType.ClampHighlights]: () => + import('anime4k-webgpu').then((m) => m.ClampHighlights), + [PipelineType.DepthToSpace]: () => + import('anime4k-webgpu').then((m) => m.DepthToSpace), + + [PipelineType.ModeA]: () => import('anime4k-webgpu').then((m) => m.ModeA), + [PipelineType.ModeB]: () => import('anime4k-webgpu').then((m) => m.ModeB), + [PipelineType.ModeC]: () => import('anime4k-webgpu').then((m) => m.ModeC), + [PipelineType.ModeAA]: () => import('anime4k-webgpu').then((m) => m.ModeAA), + [PipelineType.ModeBB]: () => import('anime4k-webgpu').then((m) => m.ModeBB), + [PipelineType.ModeCA]: () => import('anime4k-webgpu').then((m) => m.ModeCA), +}; +export interface PipelineOption { + pipeline: PipelineType; + params?: Record<string, number>; +} +export type Anime4kPipelineConstructor = new (desc: { + device: GPUDevice; + inputTexture: GPUTexture; + nativeDimensions?: { width: number; height: number }; + targetDimensions?: { width: number; height: number }; +}) => Anime4KPipeline; +export interface Anime4kPipelineWithOption { + type: PipelineType; + params?: Record<string, number>; +} diff --git a/src/renderer/filter/anime4k/index.ts b/src/renderer/filter/anime4k/index.ts index 0419db2..1e5f5a8 100644 --- a/src/renderer/filter/anime4k/index.ts +++ b/src/renderer/filter/anime4k/index.ts @@ -114,7 +114,9 @@ export async function createGpuDevice(canvas: HTMLCanvasElement) { if (!adapter) { throw new Error('WebGPU is not supported'); } - const device = await adapter.requestDevice(); + const device = await adapter.requestDevice({ + label: 'Test Anime4k GPU Device', + }); const context = canvas.getContext('webgpu') as GPUCanvasContext; const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ diff --git a/src/renderer/filter/anime4k/sampleExternalTexture.wgsl b/src/renderer/filter/anime4k/sampleExternalTexture.wgsl index 2305e0b..d501425 100644 --- a/src/renderer/filter/anime4k/sampleExternalTexture.wgsl +++ b/src/renderer/filter/anime4k/sampleExternalTexture.wgsl @@ -1,7 +1,7 @@ -@group(0) @binding(1) var mySampler: sampler; -@group(0) @binding(2) var myTexture: texture_2d<f32>; +@group(0) @binding(1) var uTexture: texture_2d<f32>; +@group(0) @binding(2) var uSampler: sampler; @fragment fn main(@location(0) fragUV : vec2f) -> @location(0) vec4f { - return textureSampleBaseClampToEdge(myTexture, mySampler, fragUV); + return textureSampleBaseClampToEdge(uTexture, uSampler, fragUV); } \ No newline at end of file From 6b0b5ebe678cb74af1ff9350981e69bee1a4fcd7 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Wed, 28 Aug 2024 00:58:43 +0800 Subject: [PATCH 07/25] [edit] filter should apply on viewport --- src/components/CharacterPreview/Character.tsx | 32 +++++++++---------- .../CharacterPreview/CharacterScene.tsx | 10 +----- src/renderer/filter/anime4k/Anime4kSystem.ts | 4 ++- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/components/CharacterPreview/Character.tsx b/src/components/CharacterPreview/Character.tsx index b95b56f..f7e0420 100644 --- a/src/components/CharacterPreview/Character.tsx +++ b/src/components/CharacterPreview/Character.tsx @@ -128,24 +128,22 @@ export const CharacterView = (props: CharacterViewProps) => { }); createEffect(async () => { - if (isShowUpscale()) { - await app.renderer.anime4k.preparePipeline([PipelineType.ModeBB]); - if (!upscaleFilter) { - upscaleFilter = new Anime4kFilter([ - { - pipeline: PipelineType.ModeBB, - // params: { - // strength: 0.5, - // strength2: 1, - // }, - } as PipelineOption, - ]); + if (isInit() && viewport) { + if (isShowUpscale()) { + await app.renderer.anime4k.preparePipeline([PipelineType.ModeBB]); + if (!upscaleFilter) { + upscaleFilter = new Anime4kFilter([ + { + pipeline: PipelineType.ModeBB, + }, + ] as PipelineOption[]); + } + /* TODO */ + upscaleFilter.updatePipeine(); + viewport.filters = [upscaleFilter]; + } else { + viewport.filters = []; } - /* TODO */ - upscaleFilter.updatePipeine(); - app.stage.filters = [upscaleFilter]; - } else { - app.stage.filters = []; } }); diff --git a/src/components/CharacterPreview/CharacterScene.tsx b/src/components/CharacterPreview/CharacterScene.tsx index c96c023..a7a4243 100644 --- a/src/components/CharacterPreview/CharacterScene.tsx +++ b/src/components/CharacterPreview/CharacterScene.tsx @@ -8,15 +8,11 @@ import { $previewCharacter, $sceneCustomColorStyle, } from '@/store/character/selector'; -import { - $showPreviousCharacter, - $showUpscaledCharacter, -} from '@/store/trigger'; +import { $showPreviousCharacter } from '@/store/trigger'; import LoaderCircle from 'lucide-solid/icons/loader-circle'; import ChevronRightIcon from 'lucide-solid/icons/chevron-right'; import { CharacterView } from './Character'; -import { UpscaleCharacter } from './UpscaleCharacter'; import { CharacterSceneSelection } from './CharacterSceneSelection'; import { ShowPreviousSwitch } from './ShowPreviousSwitch'; import { ShowUpscaleSwitch } from './ShowUpscaleSwitch'; @@ -31,7 +27,6 @@ export const CharacterScene = () => { const scene = useStore($currentScene); const customColorStyle = useStore($sceneCustomColorStyle); const isShowComparison = useStore($showPreviousCharacter); - const isShowUpscale = useStore($showUpscaledCharacter); function handleLoad() { setIsLoading(true); @@ -89,9 +84,6 @@ export const CharacterScene = () => { target="preview" isLockInteraction={isLockInteraction()} /> - <Show when={isShowUpscale()}> - <UpscaleCharacter /> - </Show> <TopTool> <ShowPreviousSwitch /> <ShowUpscaleSwitch /> diff --git a/src/renderer/filter/anime4k/Anime4kSystem.ts b/src/renderer/filter/anime4k/Anime4kSystem.ts index c45a8ac..d2a54ca 100644 --- a/src/renderer/filter/anime4k/Anime4kSystem.ts +++ b/src/renderer/filter/anime4k/Anime4kSystem.ts @@ -38,7 +38,8 @@ export class Anime4kFilterSystem implements System { new Map(); private _sizedPipelineMap: Map<string, Map<string, Anime4KPipeline>> = new Map(); - private _sizedRenderMap: Map<string, SizedRenderResourceTuple> = new Map(); + private _sizedRenderMap: Map<string, SizedRenderResourceTuple> = + new Map(); private _sizedBindGroupMap: Map<string, GPUBindGroup> = new Map(); private _gpu!: GPU; @@ -118,6 +119,7 @@ export class Anime4kFilterSystem implements System { ) { const pipelineHash = pipelines.map((p) => p.pipeline).join(','); const key = `${option.width}x${option.height}:${pipelineHash}`; + console.log(key); let resource = this._sizedRenderMap.get(key); if (!resource) { resource = this.setSizedRenderResource(option, pipelines); From ceeddfba9288ebbf429ca42dd55af1f4453bc516 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Wed, 28 Aug 2024 16:23:05 +0800 Subject: [PATCH 08/25] [add] other setting and show current version at setting --- .../OtherSetting/OpenFolderButton.tsx | 39 +++++++++++++++++++ .../SettingDialog/OtherSetting/index.tsx | 20 ++++++++++ .../RenderSetting/UpscaleSwitch.tsx | 2 +- .../SettingDialog/SettingFooter/index.tsx | 20 ++++++++++ src/components/dialog/SettingDialog/index.tsx | 4 ++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/components/dialog/SettingDialog/OtherSetting/OpenFolderButton.tsx create mode 100644 src/components/dialog/SettingDialog/OtherSetting/index.tsx create mode 100644 src/components/dialog/SettingDialog/SettingFooter/index.tsx diff --git a/src/components/dialog/SettingDialog/OtherSetting/OpenFolderButton.tsx b/src/components/dialog/SettingDialog/OtherSetting/OpenFolderButton.tsx new file mode 100644 index 0000000..58f2c84 --- /dev/null +++ b/src/components/dialog/SettingDialog/OtherSetting/OpenFolderButton.tsx @@ -0,0 +1,39 @@ +import type { JSX } from 'solid-js'; +import { appCacheDir, appDataDir } from '@tauri-apps/api/path'; +import { open } from '@tauri-apps/plugin-shell'; + +import FolderIcon from 'lucide-solid/icons/folder-symlink'; +import { Button } from '@/components/ui/button'; + +import { toaster } from '@/components/GlobalToast'; + +export enum PathType { + Data = 'data', + Cache = 'cache', +} +export interface OpenFolderButtonProps { + type: PathType; + title: string; + children: JSX.Element; +} +export const OpenFolderButton = (props: OpenFolderButtonProps) => { + async function handleClick() { + const folder = await (props.type === PathType.Data + ? appDataDir() + : appCacheDir()); + try { + await open(folder); + } catch (_) { + toaster.error({ + title: '開啟路徑時發生錯誤', + }); + } + } + + return ( + <Button onClick={handleClick} title={props.title} variant="outline"> + {props.children} + <FolderIcon /> + </Button> + ); +}; diff --git a/src/components/dialog/SettingDialog/OtherSetting/index.tsx b/src/components/dialog/SettingDialog/OtherSetting/index.tsx new file mode 100644 index 0000000..52211f7 --- /dev/null +++ b/src/components/dialog/SettingDialog/OtherSetting/index.tsx @@ -0,0 +1,20 @@ +import { Stack } from 'styled-system/jsx/stack'; +import { HStack } from 'styled-system/jsx/hstack'; +import { Heading } from '@/components/ui/heading'; +import { OpenFolderButton, PathType } from './OpenFolderButton'; + +export const OtherSetting = () => { + return ( + <Stack> + <Heading size="lg">其他</Heading> + <HStack justify="flex-start"> + <OpenFolderButton type={PathType.Data} title="開啟存檔資料夾"> + 存檔資料夾 + </OpenFolderButton> + <OpenFolderButton type={PathType.Cache} title="開啟暫存資料夾"> + 暫存資料夾 + </OpenFolderButton> + </HStack> + </Stack> + ); +}; diff --git a/src/components/dialog/SettingDialog/RenderSetting/UpscaleSwitch.tsx b/src/components/dialog/SettingDialog/RenderSetting/UpscaleSwitch.tsx index fd5b938..9c4019d 100644 --- a/src/components/dialog/SettingDialog/RenderSetting/UpscaleSwitch.tsx +++ b/src/components/dialog/SettingDialog/RenderSetting/UpscaleSwitch.tsx @@ -24,7 +24,7 @@ export const UpscaleSwitch = () => { > <HStack gap="1"> <Text>實驗性高清預覽</Text> - <SettingTooltip tooltip="新增按鈕顯示高清化的角色預覽,開啟時顯示高清版(Anime4K)的圖片在旁邊,此功能可能造成極大的效能影響,請確認有足夠的電腦資源再使用" /> + <SettingTooltip tooltip="新增按鈕顯示高清化的角色預覽,開啟時顯示高清版(Anime4K)的角色預覽,此功能可能造成一些效能影響,請確認有足夠的電腦資源再使用" /> </HStack> </Switch> ); diff --git a/src/components/dialog/SettingDialog/SettingFooter/index.tsx b/src/components/dialog/SettingDialog/SettingFooter/index.tsx new file mode 100644 index 0000000..6dd6400 --- /dev/null +++ b/src/components/dialog/SettingDialog/SettingFooter/index.tsx @@ -0,0 +1,20 @@ +import { createSignal, onMount } from 'solid-js'; +import { getVersion } from '@tauri-apps/api/app'; + +import { HStack } from 'styled-system/jsx'; +import { Text } from '@/components/ui/text'; + +export const SettingFooter = () => { + const [version, setVersion] = createSignal<string>(); + onMount(async () => { + setVersion(await getVersion()); + }); + + return ( + <HStack> + <Text marginLeft="auto" size="sm" color="fg.subtle"> + 當前版本: {version()} + </Text> + </HStack> + ); +}; diff --git a/src/components/dialog/SettingDialog/index.tsx b/src/components/dialog/SettingDialog/index.tsx index 752c79c..03c320d 100644 --- a/src/components/dialog/SettingDialog/index.tsx +++ b/src/components/dialog/SettingDialog/index.tsx @@ -4,6 +4,8 @@ import { SettingDialog as Dialog } from './SettingDialog'; import { WindowSetting } from './WindowSetting'; import { ThemeSetting } from './ThemeSetting'; import { RenderSetting } from './RenderSetting'; +import { OtherSetting } from './OtherSetting'; +import { SettingFooter } from './SettingFooter'; export const SettingDialog = () => { return ( @@ -13,6 +15,8 @@ export const SettingDialog = () => { <WindowSetting /> <ThemeSetting /> <RenderSetting /> + <OtherSetting /> + <SettingFooter /> </Stack> </Dialog> ); From 5510de9c8d782746b03b1a21278f5f2e93c84dab Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Wed, 28 Aug 2024 16:24:06 +0800 Subject: [PATCH 09/25] [edit] fix isEar match will make some item disappear --- src/renderer/character/categorizedItem.ts | 3 ++- src/renderer/filter/anime4k/Anime4kFilter.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/renderer/character/categorizedItem.ts b/src/renderer/character/categorizedItem.ts index e4b654d..9dd97ae 100644 --- a/src/renderer/character/categorizedItem.ts +++ b/src/renderer/character/categorizedItem.ts @@ -192,7 +192,8 @@ export abstract class CategorizedItem<Name extends string> { continue; } - const isEar = pieceName.match(/ear/i); + /* some thing like capeArm also has ear... so only do end with here */ + const isEar = pieceName.match(/ear$/i); const name = isEar ? pieceName diff --git a/src/renderer/filter/anime4k/Anime4kFilter.ts b/src/renderer/filter/anime4k/Anime4kFilter.ts index e4acd91..16e572e 100644 --- a/src/renderer/filter/anime4k/Anime4kFilter.ts +++ b/src/renderer/filter/anime4k/Anime4kFilter.ts @@ -13,7 +13,7 @@ import './Anime4kSystem'; import sampleExternalTextureFrag from './sampleExternalTexture.wgsl'; -import { type PipelineOption, PipelineType } from './const'; +import type { PipelineOption } from './const'; const quadGeometry = new Geometry({ attributes: { From 504a6ce1fe2ebc55adca5ac5e1f7fc5999682b1b Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Wed, 28 Aug 2024 16:53:37 +0800 Subject: [PATCH 10/25] [edit] make sure enableExperimentalUpscale is got update --- src/components/CharacterPreview/Character.tsx | 20 +++++++++++-------- src/renderer/filter/anime4k/Anime4kFilter.ts | 7 ++----- src/renderer/filter/anime4k/Anime4kSystem.ts | 1 - src/store/settingDialog.ts | 4 ++++ 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/CharacterPreview/Character.tsx b/src/components/CharacterPreview/Character.tsx index f7e0420..39104c8 100644 --- a/src/components/CharacterPreview/Character.tsx +++ b/src/components/CharacterPreview/Character.tsx @@ -130,16 +130,20 @@ export const CharacterView = (props: CharacterViewProps) => { createEffect(async () => { if (isInit() && viewport) { if (isShowUpscale()) { - await app.renderer.anime4k.preparePipeline([PipelineType.ModeBB]); + /* to be configurable in the future */ + const upscalePipelines = [ + { + pipeline: PipelineType.ModeBB, + }, + ] as PipelineOption[]; + if (!upscaleFilter) { - upscaleFilter = new Anime4kFilter([ - { - pipeline: PipelineType.ModeBB, - }, - ] as PipelineOption[]); + await app.renderer.anime4k.preparePipeline( + upscalePipelines.map((p) => p.pipeline), + ); + upscaleFilter = new Anime4kFilter(upscalePipelines); } - /* TODO */ - upscaleFilter.updatePipeine(); + // upscaleFilter.updatePipeine(upscalePipelines); viewport.filters = [upscaleFilter]; } else { viewport.filters = []; diff --git a/src/renderer/filter/anime4k/Anime4kFilter.ts b/src/renderer/filter/anime4k/Anime4kFilter.ts index 16e572e..2680c06 100644 --- a/src/renderer/filter/anime4k/Anime4kFilter.ts +++ b/src/renderer/filter/anime4k/Anime4kFilter.ts @@ -28,9 +28,6 @@ const quadGeometry = new Geometry({ }); export class Anime4kFilter extends Filter { - mainTexture: GPUTexture | undefined; - _texture: GPUTexture | undefined; - public readonly renderPipeId = 'anime4kRender'; loadedPipeline: PipelineOption[] = []; constructor(pipelines: PipelineOption[]) { @@ -134,8 +131,8 @@ export class Anime4kFilter extends Filter { 0, ); } - public updatePipeine() { - /* TODO */ + public updatePipeine(pipelines: PipelineOption[]) { + this.loadedPipeline = pipelines; } private getPixiGlobalBindGroup( filterManager: FilterSystem, diff --git a/src/renderer/filter/anime4k/Anime4kSystem.ts b/src/renderer/filter/anime4k/Anime4kSystem.ts index d2a54ca..3b1e8a7 100644 --- a/src/renderer/filter/anime4k/Anime4kSystem.ts +++ b/src/renderer/filter/anime4k/Anime4kSystem.ts @@ -119,7 +119,6 @@ export class Anime4kFilterSystem implements System { ) { const pipelineHash = pipelines.map((p) => p.pipeline).join(','); const key = `${option.width}x${option.height}:${pipelineHash}`; - console.log(key); let resource = this._sizedRenderMap.get(key); if (!resource) { resource = this.setSizedRenderResource(option, pipelines); diff --git a/src/store/settingDialog.ts b/src/store/settingDialog.ts index 758c2a4..38c7f7b 100644 --- a/src/store/settingDialog.ts +++ b/src/store/settingDialog.ts @@ -96,6 +96,10 @@ export async function initializeSavedSetting() { $appSetting.setKey('windowResizable', !!setting.windowResizable); $appSetting.setKey('showItemGender', setting.showItemGender ?? true); $appSetting.setKey('showItemDyeable', setting.showItemDyeable ?? true); + $appSetting.setKey( + 'enableExperimentalUpscale', + !!setting.enableExperimentalUpscale, + ); const defaultCharacterRendering = !!setting.defaultCharacterRendering; if (defaultCharacterRendering) { $appSetting.setKey('defaultCharacterRendering', true); From b18746a18c29c9cce68a31cd9a2cc4755e997561 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Wed, 28 Aug 2024 19:02:43 +0800 Subject: [PATCH 11/25] [add] prepare item dye table related store --- src/const/toolTab.ts | 2 ++ src/store/toolTab.ts | 64 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/const/toolTab.ts b/src/const/toolTab.ts index 68afaf4..bf794fe 100644 --- a/src/const/toolTab.ts +++ b/src/const/toolTab.ts @@ -2,12 +2,14 @@ export enum ToolTab { AllAction = 'allAction', HairDye = 'hairDye', FaceDye = 'faceDye', + ItemDye = 'itemDye', } export const ToolTabNames: Record<ToolTab, string> = { [ToolTab.AllAction]: '全部動作', [ToolTab.HairDye]: '髮型顏色', [ToolTab.FaceDye]: '臉型顏色', + [ToolTab.ItemDye]: '裝備染色表', }; export enum ActionExportType { diff --git a/src/store/toolTab.ts b/src/store/toolTab.ts index be06784..57a29a5 100644 --- a/src/store/toolTab.ts +++ b/src/store/toolTab.ts @@ -1,7 +1,69 @@ -import { atom } from 'nanostores'; +import { atom, deepMap } from 'nanostores'; +import type { EquipSubCategory } from '@/const/equipments'; import { type ToolTab, ActionExportType } from '@/const/toolTab'; +import { CharacterAction } from '@/const/actions'; export const $toolTab = atom<ToolTab | undefined>(undefined); export const $actionExportType = atom<ActionExportType>(ActionExportType.Gif); + +export const $onlyShowDyeable = atom<boolean>(false); +export const $preserveOriginalDye = atom<boolean>(false); +export const $selectedEquipSubCategory = atom<EquipSubCategory[]>([]); +export const $dyeResultCount = atom<number>(72); +export const $dyeAction = atom<CharacterAction>(CharacterAction.Stand1); + +export enum DyeOrder { + Up = 'up', + Down = 'down', +} +export interface DyeConfigOption { + enabled: boolean; + order: DyeOrder; +} +export type DyeConfig = { + hue: DyeConfigOption; + saturation: DyeConfigOption; + lightness: DyeConfigOption; +}; +export const $dyeConfig = deepMap({ + hue: { + enabled: false, + order: DyeOrder.Up, + }, + saturation: { + enabled: false, + order: DyeOrder.Up, + }, + lightness: { + enabled: false, + order: DyeOrder.Up, + }, +}); + +/* actions */ +export function toggleDyeConfigEnabled(key: keyof DyeConfig, value: boolean) { + $dyeConfig.setKey(`${key}.enabled`, value); +} +export function toggleDyeConfigOrder(key: keyof DyeConfig, order: DyeOrder) { + $dyeConfig.setKey(`${key}.order`, order); +} +export function selectDyeCategory(category: EquipSubCategory) { + const current = $selectedEquipSubCategory.get(); + if (current.includes(category)) { + return; + } + $selectedEquipSubCategory.set([...$selectedEquipSubCategory.get(), category]); +} +export function deselectDyeCategory(category: EquipSubCategory) { + $selectedEquipSubCategory.set( + $selectedEquipSubCategory.get().filter((c) => c !== category), + ); +} +export function setDyeResultCount(count: number) { + $dyeResultCount.set(count); +} +export function setDyeAction(action: CharacterAction) { + $dyeAction.set(action); +} From aa33138ad5bf9836d3ff0c7d2d66f122a000824e Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Thu, 29 Aug 2024 11:08:23 +0800 Subject: [PATCH 12/25] [add] IconTooltip --- .../dialog/SettingDialog/SettingTooltip.tsx | 7 ++--- src/components/elements/IconTooltip.tsx | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 src/components/elements/IconTooltip.tsx diff --git a/src/components/dialog/SettingDialog/SettingTooltip.tsx b/src/components/dialog/SettingDialog/SettingTooltip.tsx index 7cfdfd6..6785c21 100644 --- a/src/components/dialog/SettingDialog/SettingTooltip.tsx +++ b/src/components/dialog/SettingDialog/SettingTooltip.tsx @@ -1,13 +1,10 @@ -import InfoIcon from 'lucide-solid/icons/info'; -import { SimpleTooltip } from '@/components/ui/tooltip'; +import { IconTooltop, IconType } from '@/components/elements/IconTooltip'; export interface SettingTooltipProps { tooltip: string; } export const SettingTooltip = (props: SettingTooltipProps) => { return ( - <SimpleTooltip zIndex={2300} tooltip={props.tooltip}> - <InfoIcon color="currentColor" size="16" /> - </SimpleTooltip> + <IconTooltop type={IconType.Info} zIndex={2300} tooltip={props.tooltip} /> ); }; diff --git a/src/components/elements/IconTooltip.tsx b/src/components/elements/IconTooltip.tsx new file mode 100644 index 0000000..2b643b1 --- /dev/null +++ b/src/components/elements/IconTooltip.tsx @@ -0,0 +1,29 @@ +import { Match, Switch } from 'solid-js'; +import InfoIcon from 'lucide-solid/icons/info'; +import CircleHelpIcon from 'lucide-solid/icons/circle-help'; +import { SimpleTooltip } from '@/components/ui/tooltip'; + +export enum IconType { + Info = 'info', + Question = 'question', +} +export interface IconTooltopProps { + tooltip: string; + type: IconType; + zIndex?: number; + size?: number; +} +export const IconTooltop = (props: IconTooltopProps) => { + return ( + <SimpleTooltip zIndex={props.zIndex} tooltip={props.tooltip}> + <Switch> + <Match when={props.type === IconType.Info}> + <InfoIcon color="currentColor" size={props.size || '16'} /> + </Match> + <Match when={props.type === IconType.Question}> + <CircleHelpIcon color="currentColor" size={props.size || '16'} /> + </Match> + </Switch> + </SimpleTooltip> + ); +}; From 6fbac26573d0049e5ec69e027234bbb75ed83b5b Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Thu, 29 Aug 2024 15:48:43 +0800 Subject: [PATCH 13/25] [add] radioGroup base ui --- src/components/ui/radioGroup.tsx | 49 +++++++++++++++++++++++++++++++ src/components/ui/toggleGroup.tsx | 6 ++-- 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 src/components/ui/radioGroup.tsx diff --git a/src/components/ui/radioGroup.tsx b/src/components/ui/radioGroup.tsx new file mode 100644 index 0000000..9bf6eeb --- /dev/null +++ b/src/components/ui/radioGroup.tsx @@ -0,0 +1,49 @@ +import { type Assign, RadioGroup } from '@ark-ui/solid'; +import type { ComponentProps } from 'solid-js'; +import { type RadioGroupVariantProps, radioGroup } from 'styled-system/recipes'; +import type { HTMLStyledProps } from 'styled-system/types'; +import { createStyleContext } from '@/utils/create-style-context'; + +const { withProvider, withContext } = createStyleContext(radioGroup); + +export type RootProviderProps = ComponentProps<typeof RootProvider>; +export const RootProvider = withProvider< + Assign< + Assign<HTMLStyledProps<'div'>, RadioGroup.RootProviderBaseProps>, + RadioGroupVariantProps + > +>(RadioGroup.RootProvider, 'root'); + +export type RootProps = ComponentProps<typeof Root>; +export const Root = withProvider< + Assign< + Assign<HTMLStyledProps<'div'>, RadioGroup.RootBaseProps>, + RadioGroupVariantProps + > +>(RadioGroup.Root, 'root'); + +export const Indicator = withContext< + Assign<HTMLStyledProps<'div'>, RadioGroup.IndicatorBaseProps> +>(RadioGroup.Indicator, 'indicator'); + +export const ItemControl = withContext< + Assign<HTMLStyledProps<'div'>, RadioGroup.ItemControlBaseProps> +>(RadioGroup.ItemControl, 'itemControl'); + +export const Item = withContext< + Assign<HTMLStyledProps<'label'>, RadioGroup.ItemBaseProps> +>(RadioGroup.Item, 'item'); + +export const ItemText = withContext< + Assign<HTMLStyledProps<'span'>, RadioGroup.ItemTextBaseProps> +>(RadioGroup.ItemText, 'itemText'); + +export const Label = withContext< + Assign<HTMLStyledProps<'label'>, RadioGroup.LabelBaseProps> +>(RadioGroup.Label, 'label'); + +export { + RadioGroupContext as Context, + RadioGroupItemHiddenInput as ItemHiddenInput, + type RadioGroupValueChangeDetails as ValueChangeDetails, +} from '@ark-ui/solid'; diff --git a/src/components/ui/toggleGroup.tsx b/src/components/ui/toggleGroup.tsx index b18e0a8..c51b24d 100644 --- a/src/components/ui/toggleGroup.tsx +++ b/src/components/ui/toggleGroup.tsx @@ -16,10 +16,8 @@ export interface RootProps ToggleGroupVariantProps {} export const Root = withProvider<RootProps>(ToggleGroup.Root, 'root'); -export const Item = withContext<Assign<JsxStyleProps, ToggleGroup.ItemProps>>( - ToggleGroup.Item, - 'item', -); +export type ItemProps = Assign<JsxStyleProps, ToggleGroup.ItemProps>; +export const Item = withContext<ItemProps>(ToggleGroup.Item, 'item'); export { ToggleGroupContext as Context, From 69bf49cab0996e38e75b2db62c8d71637b32d64c Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Thu, 29 Aug 2024 15:50:04 +0800 Subject: [PATCH 14/25] [edit] move sharable part of ActionSelect to elements --- .../CharacterPreview/ActionSelect.tsx | 94 +--------------- src/components/elements/ActionSelect.tsx | 100 ++++++++++++++++++ src/components/elements/LoadableEquipIcon.tsx | 6 +- 3 files changed, 110 insertions(+), 90 deletions(-) create mode 100644 src/components/elements/ActionSelect.tsx diff --git a/src/components/CharacterPreview/ActionSelect.tsx b/src/components/CharacterPreview/ActionSelect.tsx index aa7c4b7..96471c7 100644 --- a/src/components/CharacterPreview/ActionSelect.tsx +++ b/src/components/CharacterPreview/ActionSelect.tsx @@ -3,101 +3,17 @@ import { useStore } from '@nanostores/solid'; import { $currentCharacterInfo } from '@/store/character/store'; import { $currentAction } from '@/store/character/selector'; -import { SimpleSelect, type ValueChangeDetails } from '@/components/ui/select'; +import { ActionSelect as BaseActionSelect } from '@/components/elements/ActionSelect'; -import { CharacterAction } from '@/const/actions'; - -const options = [ - { - label: '站立', - value: CharacterAction.Stand1, - }, - { - label: '坐下', - value: CharacterAction.Sit, - }, - { - label: '走路', - value: CharacterAction.Walk1, - }, - { - label: '跳躍', - value: CharacterAction.Jump, - }, - { - label: '飛行/游泳', - value: CharacterAction.Fly, - }, - { - label: '攀爬(梯子)', - value: CharacterAction.Ladder, - }, - { - label: '攀爬(繩子)', - value: CharacterAction.Rope, - }, - { - label: '警戒', - value: CharacterAction.Alert, - }, - { - label: '施放', - value: CharacterAction.Heal, - }, - { - label: '趴下', - value: CharacterAction.Prone, - }, - { - label: '趴下攻擊', - value: CharacterAction.ProneStab, - }, - ...[ - CharacterAction.Shoot1, - CharacterAction.Shoot2, - CharacterAction.ShootF, - CharacterAction.Sit, - CharacterAction.StabO1, - CharacterAction.StabO2, - CharacterAction.StabOF, - CharacterAction.StabT1, - CharacterAction.StabT2, - CharacterAction.StabTF, - CharacterAction.SwingO1, - CharacterAction.SwingO2, - CharacterAction.SwingO3, - CharacterAction.SwingOF, - CharacterAction.SwingP1, - CharacterAction.SwingP2, - CharacterAction.SwingPF, - CharacterAction.SwingT1, - CharacterAction.SwingT2, - CharacterAction.SwingT3, - CharacterAction.SwingTF, - ].map((value) => ({ - label: value, - value, - })), -]; +import type { CharacterAction } from '@/const/actions'; export const ActionSelect = () => { const action = useStore($currentAction); - function handleActionChange(details: ValueChangeDetails) { - const firstItem = details.value?.[0]; - firstItem && - $currentCharacterInfo.setKey('action', firstItem as CharacterAction); + function handleActionChange(action: CharacterAction | undefined) { + action && $currentCharacterInfo.setKey('action', action); } return ( - <SimpleSelect - positioning={{ - sameWidth: true, - }} - items={options} - value={[action()]} - onValueChange={handleActionChange} - groupTitle="角色動作" - maxHeight="20rem" - /> + <BaseActionSelect value={action()} onValueChange={handleActionChange} /> ); }; diff --git a/src/components/elements/ActionSelect.tsx b/src/components/elements/ActionSelect.tsx new file mode 100644 index 0000000..74a39c7 --- /dev/null +++ b/src/components/elements/ActionSelect.tsx @@ -0,0 +1,100 @@ +import { SimpleSelect, type ValueChangeDetails } from '@/components/ui/select'; + +import { CharacterAction } from '@/const/actions'; + +const options = [ + { + label: '站立', + value: CharacterAction.Stand1, + }, + { + label: '坐下', + value: CharacterAction.Sit, + }, + { + label: '走路', + value: CharacterAction.Walk1, + }, + { + label: '跳躍', + value: CharacterAction.Jump, + }, + { + label: '飛行/游泳', + value: CharacterAction.Fly, + }, + { + label: '攀爬(梯子)', + value: CharacterAction.Ladder, + }, + { + label: '攀爬(繩子)', + value: CharacterAction.Rope, + }, + { + label: '警戒', + value: CharacterAction.Alert, + }, + { + label: '施放', + value: CharacterAction.Heal, + }, + { + label: '趴下', + value: CharacterAction.Prone, + }, + { + label: '趴下攻擊', + value: CharacterAction.ProneStab, + }, + ...[ + CharacterAction.Shoot1, + CharacterAction.Shoot2, + CharacterAction.ShootF, + CharacterAction.Sit, + CharacterAction.StabO1, + CharacterAction.StabO2, + CharacterAction.StabOF, + CharacterAction.StabT1, + CharacterAction.StabT2, + CharacterAction.StabTF, + CharacterAction.SwingO1, + CharacterAction.SwingO2, + CharacterAction.SwingO3, + CharacterAction.SwingOF, + CharacterAction.SwingP1, + CharacterAction.SwingP2, + CharacterAction.SwingPF, + CharacterAction.SwingT1, + CharacterAction.SwingT2, + CharacterAction.SwingT3, + CharacterAction.SwingTF, + ].map((value) => ({ + label: value, + value, + })), +]; + +export interface ActionSelectProps { + value: CharacterAction; + onValueChange: (value: CharacterAction | undefined) => void; +} +export const ActionSelect = (props: ActionSelectProps) => { + function handleActionChange(details: ValueChangeDetails) { + const firstItem = details.value?.[0] as CharacterAction | undefined; + props.onValueChange(firstItem); + } + + return ( + <SimpleSelect + positioning={{ + sameWidth: true, + }} + items={options} + value={[props.value]} + onValueChange={handleActionChange} + groupTitle="角色動作" + maxHeight="20rem" + /> + ); +}; diff --git a/src/components/elements/LoadableEquipIcon.tsx b/src/components/elements/LoadableEquipIcon.tsx index d6e68a0..4d1530d 100644 --- a/src/components/elements/LoadableEquipIcon.tsx +++ b/src/components/elements/LoadableEquipIcon.tsx @@ -64,7 +64,11 @@ export const LoadableEquipIcon = (props: LoadableEquipIconProps) => { alignItems="center" isLoaded={isLoaded()} > - <IconContainer gender={gender()}> + <IconContainer + gender={gender()} + width={props.width} + height={props.height} + > <Show when={!isError()} fallback={<CircleHelpIcon />}> <img {...contextTriggerProps} From 28bb198e1d9c5f948d12063818b7a87bad8846e3 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Thu, 29 Aug 2024 15:50:31 +0800 Subject: [PATCH 15/25] [add] ItemDyeTab settings --- src/components/ToolTabPage.tsx | 4 + src/components/ToolTabsRadioGroup.tsx | 4 + .../tab/ItemDyeTab/DyeTypeRadioGroup.tsx | 63 +++++++++ .../tab/ItemDyeTab/ItemDyeTabTitle.tsx | 16 +++ src/components/tab/ItemDyeTab/NeedDyeItem.tsx | 133 ++++++++++++++++++ .../tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx | 56 ++++++++ .../tab/ItemDyeTab/OnlyDyeableSwitch.tsx | 20 +++ .../tab/ItemDyeTab/PreserveDyeSwitch.tsx | 28 ++++ .../tab/ItemDyeTab/ResultActionSelect.tsx | 18 +++ .../tab/ItemDyeTab/ResultCountNumberInput.tsx | 27 ++++ src/components/tab/ItemDyeTab/index.tsx | 52 +++++++ src/const/toolTab.ts | 10 ++ src/store/toolTab.ts | 41 ++++-- 13 files changed, 461 insertions(+), 11 deletions(-) create mode 100644 src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx create mode 100644 src/components/tab/ItemDyeTab/ItemDyeTabTitle.tsx create mode 100644 src/components/tab/ItemDyeTab/NeedDyeItem.tsx create mode 100644 src/components/tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx create mode 100644 src/components/tab/ItemDyeTab/OnlyDyeableSwitch.tsx create mode 100644 src/components/tab/ItemDyeTab/PreserveDyeSwitch.tsx create mode 100644 src/components/tab/ItemDyeTab/ResultActionSelect.tsx create mode 100644 src/components/tab/ItemDyeTab/ResultCountNumberInput.tsx create mode 100644 src/components/tab/ItemDyeTab/index.tsx diff --git a/src/components/ToolTabPage.tsx b/src/components/ToolTabPage.tsx index 1c76d8e..5f9251d 100644 --- a/src/components/ToolTabPage.tsx +++ b/src/components/ToolTabPage.tsx @@ -6,6 +6,7 @@ import { $toolTab } from '@/store/toolTab'; import { ActionTab } from './tab/ActionTab'; import { HairDyeTab } from './tab/DyeTab/HairDyeTab'; import { FaceDyeTab } from './tab/DyeTab/FaceDyeTab'; +import { ItemDyeTab } from './tab/ItemDyeTab'; import { ToolTab } from '@/const/toolTab'; @@ -23,6 +24,9 @@ export const ToolTabPage = () => { <Match when={tab() === ToolTab.FaceDye}> <FaceDyeTab /> </Match> + <Match when={tab() === ToolTab.ItemDye}> + <ItemDyeTab /> + </Match> </Switch> ); }; diff --git a/src/components/ToolTabsRadioGroup.tsx b/src/components/ToolTabsRadioGroup.tsx index b55b78f..0435c77 100644 --- a/src/components/ToolTabsRadioGroup.tsx +++ b/src/components/ToolTabsRadioGroup.tsx @@ -24,6 +24,10 @@ const options = [ value: ToolTab.FaceDye, label: ToolTabNames[ToolTab.FaceDye], }, + { + value: ToolTab.ItemDye, + label: ToolTabNames[ToolTab.ItemDye], + }, ]; export const ToolTabsRadioGroup = () => { diff --git a/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx b/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx new file mode 100644 index 0000000..9203ccc --- /dev/null +++ b/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx @@ -0,0 +1,63 @@ +import { styled } from 'styled-system/jsx/factory'; +import { useStore } from '@nanostores/solid'; + +import { $dyeTypeEnabled, toggleDyeConfigEnabled } from '@/store/toolTab'; + +import { HStack } from 'styled-system/jsx/hstack'; +import * as RadioGroup from '@/components/ui/radioGroup'; + +import { DyeType } from '@/const/toolTab'; + +export const DyeTypeRadioGroup = () => { + const dyeTypeEnabled = useStore($dyeTypeEnabled); + + const handleValueChange = (value: RadioGroup.ValueChangeDetails) => { + toggleDyeConfigEnabled(value.value as DyeType, true); + }; + + return ( + <RadioGroup.Root + width="full" + orientation="horizontal" + value={dyeTypeEnabled()} + onValueChange={handleValueChange} + > + {/* <HStack width="full" gap="3"> */} + <RadioGroup.Item value={DyeType.Hue}> + <RadioGroup.ItemControl /> + <RadioGroup.ItemText> + 色相 + <ColorBlock backgroundGradient="hueConic" /> + </RadioGroup.ItemText> + <RadioGroup.ItemHiddenInput /> + </RadioGroup.Item> + <RadioGroup.Item value={DyeType.Saturation}> + <RadioGroup.ItemControl /> + <RadioGroup.ItemText> + 飽和度 + <ColorBlock backgroundGradient="saturation" /> + </RadioGroup.ItemText> + <RadioGroup.ItemHiddenInput /> + </RadioGroup.Item> + <RadioGroup.Item value={DyeType.Lightness}> + <RadioGroup.ItemControl /> + <RadioGroup.ItemText> + 亮度 + <ColorBlock backgroundGradient="brightness" /> + </RadioGroup.ItemText> + <RadioGroup.ItemHiddenInput /> + </RadioGroup.Item> + {/* </HStack> */} + </RadioGroup.Root> + ); +}; + +const ColorBlock = styled('div', { + base: { + borderRadius: 'sm', + w: 3, + h: 3, + display: 'inline-block', + ml: 2, + }, +}); diff --git a/src/components/tab/ItemDyeTab/ItemDyeTabTitle.tsx b/src/components/tab/ItemDyeTab/ItemDyeTabTitle.tsx new file mode 100644 index 0000000..a1c0b1b --- /dev/null +++ b/src/components/tab/ItemDyeTab/ItemDyeTabTitle.tsx @@ -0,0 +1,16 @@ +import { HStack } from 'styled-system/jsx/hstack'; +import { Heading } from '@/components/ui/heading'; +import { OnlyDyeableSwitch } from './OnlyDyeableSwitch'; +import { PreserveDyeSwitch } from './PreserveDyeSwitch'; + +export const ItemDyeTabTitle = () => { + return ( + <HStack justify="flex-start"> + <Heading size="2xl" marginRight="4"> + 裝備染色表 + </Heading> + <OnlyDyeableSwitch /> + <PreserveDyeSwitch /> + </HStack> + ); +}; diff --git a/src/components/tab/ItemDyeTab/NeedDyeItem.tsx b/src/components/tab/ItemDyeTab/NeedDyeItem.tsx new file mode 100644 index 0000000..50527fa --- /dev/null +++ b/src/components/tab/ItemDyeTab/NeedDyeItem.tsx @@ -0,0 +1,133 @@ +import { Show, createMemo, createEffect, splitProps } from 'solid-js'; +import { useStore } from '@nanostores/solid'; +import { styled } from 'styled-system/jsx/factory'; + +import type { CharacterItemInfo } from '@/store/character/store'; +import { createEquipItemByCategory } from '@/store/character/selector'; +import { getEquipById } from '@/store/string'; + +import CheckIcon from 'lucide-solid/icons/check'; +import type { ItemProps } from '@/components/ui/toggleGroup'; +import { LoadableEquipIcon } from '@/components/elements/LoadableEquipIcon'; +import { EllipsisText } from '@/components/ui/ellipsisText'; +import { + EquipItemIcon, + EquipItemName, + EquipItemInfo, +} from '@/components/drawer/CurrentEquipmentDrawer/EquipItem'; +import { ItemNotExistMask } from '@/components/drawer/CurrentEquipmentDrawer/ItemNotExistMask'; + +import type { EquipSubCategory } from '@/const/equipments'; + +type ItemChildProps = NonNullable< + Parameters<Parameters<NonNullable<ItemProps['asChild']>>[0]>[0] +>; +export interface NeedDyeItemProps extends ItemChildProps { + category: EquipSubCategory; + onlyShowDyeable?: boolean; +} +export const NeedDyeItem = (props: NeedDyeItemProps) => { + const [ownProps, buttonProps] = splitProps(props, [ + 'category', + 'onlyShowDyeable', + 'class', + ]); + const item = useStore(createEquipItemByCategory(ownProps.category)); + + const equipInfo = createMemo(() => { + const id = item()?.id; + if (!id) { + return; + } + return getEquipById(id); + }); + + return ( + <Show when={props.onlyShowDyeable ? equipInfo()?.isDyeable : true}> + <Show when={equipInfo()}> + {(item) => ( + <SelectableContainer {...buttonProps}> + <EquipItemIcon> + <LoadableEquipIcon + width="7" + height="7" + id={item().id} + name={item().name} + isDyeable={item().isDyeable} + /> + </EquipItemIcon> + <EquipItemInfo gap="0"> + <EquipItemName> + <Show when={item().name} fallback={item().id}> + <EllipsisText as="div" title={item().name}> + {item().name} + </EllipsisText> + </Show> + </EquipItemName> + </EquipItemInfo> + <CheckIconContainer class="check-icon"> + <CheckIcon size="1em" /> + </CheckIconContainer> + </SelectableContainer> + )} + </Show> + </Show> + ); +}; + +const SelectableContainer = styled('button', { + base: { + display: 'grid', + py: '1', + px: '2', + borderRadius: 'md', + width: 'full', + gridTemplateColumns: 'auto 1fr', + alignItems: 'center', + position: 'relative', + cursor: 'pointer', + borderWidth: '2px', + borderColor: 'colorPalette.a7', + color: 'colorPalette.text', + colorPalette: 'gray', + _hover: { + background: 'colorPalette.a2', + }, + _disabled: { + borderColor: 'border.disabled', + color: 'fg.disabled', + cursor: 'not-allowed', + _hover: { + background: 'transparent', + borderColor: 'border.disabled', + color: 'fg.disabled', + }, + }, + _focusVisible: { + outline: '2px solid', + outlineColor: 'colorPalette.default', + outlineOffset: '2px', + }, + _on: { + borderColor: 'accent.default', + color: 'accent.fg', + _hover: { + borderColor: 'accent.emphasized', + }, + '&> .check-icon': { + display: 'block', + }, + }, + }, +}); + +const CheckIconContainer = styled('div', { + base: { + position: 'absolute', + right: '-1', + top: '-1', + backgroundColor: 'accent.default', + display: 'none', + borderRadius: '50%', + }, +}); diff --git a/src/components/tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx b/src/components/tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx new file mode 100644 index 0000000..80ecbc1 --- /dev/null +++ b/src/components/tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx @@ -0,0 +1,56 @@ +import { For, Index } from 'solid-js'; +import { useStore } from '@nanostores/solid'; + +import { $onlyShowDyeable } from '@/store/toolTab'; + +import { Grid } from 'styled-system/jsx/grid'; +import * as ToggleGroup from '@/components/ui/toggleGroup'; + +import { NeedDyeItem } from './NeedDyeItem'; +import type { EquipSubCategory } from '@/const/equipments'; + +const CategoryList = [ + 'Head', + 'Weapon', + 'Cap', + 'Cape', + 'Coat', + 'Glove', + 'Overall', + 'Pants', + 'Shield', + 'Shoes', + 'Face Accessory', + 'Eye Decoration', + 'Earrings', +] as EquipSubCategory[]; + +export const NeedDyeItemToggleGroup = () => { + const onlyShowDyeable = useStore($onlyShowDyeable); + + return ( + <ToggleGroup.Root + multiple={true} + width="full" + py="0.5" + borderColor="transparent" + > + <Grid width="full" columns={7}> + <Index each={CategoryList}> + {(category) => ( + <ToggleGroup.Item + value={category()} + asChild={(props) => ( + <NeedDyeItem + category={category()} + onlyShowDyeable={onlyShowDyeable()} + {...props()} + /> + )} + /> + )} + </Index> + </Grid> + </ToggleGroup.Root> + ); +}; diff --git a/src/components/tab/ItemDyeTab/OnlyDyeableSwitch.tsx b/src/components/tab/ItemDyeTab/OnlyDyeableSwitch.tsx new file mode 100644 index 0000000..bed1489 --- /dev/null +++ b/src/components/tab/ItemDyeTab/OnlyDyeableSwitch.tsx @@ -0,0 +1,20 @@ +import { useStore } from '@nanostores/solid'; + +import { $onlyShowDyeable } from '@/store/toolTab'; + +import { Text } from '@/components/ui/text'; +import { Switch, type ChangeDetails } from '@/components/ui/switch'; + +export const OnlyDyeableSwitch = () => { + const checked = useStore($onlyShowDyeable); + + function handleChange(details: ChangeDetails) { + $onlyShowDyeable.set(details.checked); + } + + return ( + <Switch checked={checked()} onCheckedChange={handleChange}> + <Text>僅顯示可染色裝備</Text> + </Switch> + ); +}; diff --git a/src/components/tab/ItemDyeTab/PreserveDyeSwitch.tsx b/src/components/tab/ItemDyeTab/PreserveDyeSwitch.tsx new file mode 100644 index 0000000..5a8df10 --- /dev/null +++ b/src/components/tab/ItemDyeTab/PreserveDyeSwitch.tsx @@ -0,0 +1,28 @@ +import { useStore } from '@nanostores/solid'; + +import { $preserveOriginalDye } from '@/store/toolTab'; + +import { HStack } from 'styled-system/jsx/hstack'; +import { Text } from '@/components/ui/text'; +import { Switch, type ChangeDetails } from '@/components/ui/switch'; +import { IconTooltop, IconType } from '@/components/elements/IconTooltip'; + +export const PreserveDyeSwitch = () => { + const checked = useStore($preserveOriginalDye); + + function handleChange(details: ChangeDetails) { + $preserveOriginalDye.set(details.checked); + } + + return ( + <Switch checked={checked()} onCheckedChange={handleChange}> + <HStack gap="1"> + <Text>保留裝備染色</Text> + <IconTooltop + type={IconType.Question} + tooltip="保留原裝備染色,關閉後將會重製所有染色才套用染色預覽" + /> + </HStack> + </Switch> + ); +}; diff --git a/src/components/tab/ItemDyeTab/ResultActionSelect.tsx b/src/components/tab/ItemDyeTab/ResultActionSelect.tsx new file mode 100644 index 0000000..b27f325 --- /dev/null +++ b/src/components/tab/ItemDyeTab/ResultActionSelect.tsx @@ -0,0 +1,18 @@ +import { useStore } from '@nanostores/solid'; + +import { $dyeAction } from '@/store/toolTab'; + +import { ActionSelect as BaseActionSelect } from '@/components/elements/ActionSelect'; + +import type { CharacterAction } from '@/const/actions'; + +export const ResultActionSelect = () => { + const action = useStore($dyeAction); + function handleActionChange(action: CharacterAction | undefined) { + action && $dyeAction.set(action); + } + + return ( + <BaseActionSelect value={action()} onValueChange={handleActionChange} /> + ); +}; diff --git a/src/components/tab/ItemDyeTab/ResultCountNumberInput.tsx b/src/components/tab/ItemDyeTab/ResultCountNumberInput.tsx new file mode 100644 index 0000000..851b139 --- /dev/null +++ b/src/components/tab/ItemDyeTab/ResultCountNumberInput.tsx @@ -0,0 +1,27 @@ +import { useStore } from '@nanostores/solid'; + +import { $dyeResultCount } from '@/store/toolTab'; + +import { + NumberInput, + type ValueChangeDetails, +} from '@/components/ui/numberInput'; + +export const ResultCountNumberInput = () => { + const count = useStore($dyeResultCount); + + function handleCountChange(details: ValueChangeDetails) { + $dyeResultCount.set(details.valueAsNumber); + } + + return ( + <NumberInput + min={32} + max={200} + value={count().toString()} + onValueChange={handleCountChange} + allowOverflow={false} + width="6rem" + /> + ); +}; diff --git a/src/components/tab/ItemDyeTab/index.tsx b/src/components/tab/ItemDyeTab/index.tsx new file mode 100644 index 0000000..a36ad76 --- /dev/null +++ b/src/components/tab/ItemDyeTab/index.tsx @@ -0,0 +1,52 @@ +import { styled } from 'styled-system/jsx/factory'; + +import { Stack } from 'styled-system/jsx/stack'; + +import { HStack } from 'styled-system/jsx/hstack'; +import { Text } from '@/components/ui/text'; +import { Heading } from '@/components/ui/heading'; +import { ItemDyeTabTitle } from './ItemDyeTabTitle'; +import { NeedDyeItemToggleGroup } from './NeedDyeItemToggleGroup'; +import { DyeTypeRadioGroup } from './DyeTypeRadioGroup'; +import { ResultCountNumberInput } from './ResultCountNumberInput'; +import { ResultActionSelect } from './ResultActionSelect'; + +export const ItemDyeTab = () => { + return ( + <Stack> + <CardContainer> + <ItemDyeTabTitle /> + <HStack> + <Heading width="7rem">欲染色裝備</Heading> + <NeedDyeItemToggleGroup /> + </HStack> + <HStack> + <Heading width="7rem">染色類型</Heading> + <DyeTypeRadioGroup /> + </HStack> + <HStack> + <Heading width="7rem">其他設定</Heading> + <HStack> + <Text width="7rem">染色動作</Text> + <ResultActionSelect /> + </HStack> + <HStack> + <Text>染色結果數量</Text> + <ResultCountNumberInput /> + </HStack> + </HStack> + </CardContainer> + <CardContainer></CardContainer> + </Stack> + ); +}; + +export const CardContainer = styled(Stack, { + base: { + p: 4, + borderRadius: 'md', + boxShadow: 'md', + backgroundColor: 'bg.default', + width: '100%', + }, +}); diff --git a/src/const/toolTab.ts b/src/const/toolTab.ts index bf794fe..2fac4aa 100644 --- a/src/const/toolTab.ts +++ b/src/const/toolTab.ts @@ -29,3 +29,13 @@ export const ActionExportTypeMimeType: Record<ActionExportType, string> = { [ActionExportType.Apng]: 'image/png', [ActionExportType.Webp]: 'image/webp', }; + +export enum DyeOrder { + Up = 'up', + Down = 'down', +} +export enum DyeType { + Hue = 'hue', + Saturation = 'saturation', + Lightness = 'lightness', +} diff --git a/src/store/toolTab.ts b/src/store/toolTab.ts index 57a29a5..ac09063 100644 --- a/src/store/toolTab.ts +++ b/src/store/toolTab.ts @@ -1,23 +1,24 @@ -import { atom, deepMap } from 'nanostores'; +import { atom, deepMap, computed, batched } from 'nanostores'; import type { EquipSubCategory } from '@/const/equipments'; -import { type ToolTab, ActionExportType } from '@/const/toolTab'; +import { + type ToolTab, + ActionExportType, + DyeOrder, + DyeType, +} from '@/const/toolTab'; import { CharacterAction } from '@/const/actions'; export const $toolTab = atom<ToolTab | undefined>(undefined); export const $actionExportType = atom<ActionExportType>(ActionExportType.Gif); -export const $onlyShowDyeable = atom<boolean>(false); -export const $preserveOriginalDye = atom<boolean>(false); +export const $onlyShowDyeable = atom<boolean>(true); +export const $preserveOriginalDye = atom<boolean>(true); export const $selectedEquipSubCategory = atom<EquipSubCategory[]>([]); export const $dyeResultCount = atom<number>(72); export const $dyeAction = atom<CharacterAction>(CharacterAction.Stand1); -export enum DyeOrder { - Up = 'up', - Down = 'down', -} export interface DyeConfigOption { enabled: boolean; order: DyeOrder; @@ -42,11 +43,29 @@ export const $dyeConfig = deepMap({ }, }); -/* actions */ -export function toggleDyeConfigEnabled(key: keyof DyeConfig, value: boolean) { +/* selector */ +export const $dyeTypeEnabled = batched($dyeConfig, (config) => { + for (const k of Object.values(DyeType) as DyeType[]) { + if (config[k].enabled) { + return k; + } + } + return undefined; +}); + +/* action */ +export function disableOtherDyeConfig(key: DyeType) { + for (const k of Object.values(DyeType) as DyeType[]) { + if (k !== key) { + $dyeConfig.setKey(`${k}.enabled`, false); + } + } +} +export function toggleDyeConfigEnabled(key: DyeType, value: boolean) { $dyeConfig.setKey(`${key}.enabled`, value); + disableOtherDyeConfig(key); } -export function toggleDyeConfigOrder(key: keyof DyeConfig, order: DyeOrder) { +export function toggleDyeConfigOrder(key: DyeType, order: DyeOrder) { $dyeConfig.setKey(`${key}.order`, order); } export function selectDyeCategory(category: EquipSubCategory) { From 66151da07130fca769a56bd763d0ef7ba783150a Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Thu, 29 Aug 2024 19:05:15 +0800 Subject: [PATCH 16/25] [add] dye table and export function from hair&face dye --- .../tab/ItemDyeTab/DyeCharacter.tsx | 62 ++++++ src/components/tab/ItemDyeTab/DyeInfo.tsx | 56 +++++ src/components/tab/ItemDyeTab/DyeResult.tsx | 56 +++++ .../tab/ItemDyeTab/DyeResultTable.tsx | 203 ++++++++++++++++++ .../tab/ItemDyeTab/DyeTypeRadioGroup.tsx | 10 +- .../tab/ItemDyeTab/ExportTableButton.tsx | 79 +++++++ src/components/tab/ItemDyeTab/NeedDyeItem.tsx | 6 +- .../tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx | 19 +- .../ResultColumnCountNumberInput.tsx | 27 +++ .../tab/ItemDyeTab/StartDyeButton.tsx | 63 ++++++ src/components/tab/ItemDyeTab/index.tsx | 11 +- src/const/toolTab.ts | 2 +- src/store/toolTab.ts | 28 ++- src/utils/extract.ts | 8 + 14 files changed, 601 insertions(+), 29 deletions(-) create mode 100644 src/components/tab/ItemDyeTab/DyeCharacter.tsx create mode 100644 src/components/tab/ItemDyeTab/DyeInfo.tsx create mode 100644 src/components/tab/ItemDyeTab/DyeResult.tsx create mode 100644 src/components/tab/ItemDyeTab/DyeResultTable.tsx create mode 100644 src/components/tab/ItemDyeTab/ExportTableButton.tsx create mode 100644 src/components/tab/ItemDyeTab/ResultColumnCountNumberInput.tsx create mode 100644 src/components/tab/ItemDyeTab/StartDyeButton.tsx diff --git a/src/components/tab/ItemDyeTab/DyeCharacter.tsx b/src/components/tab/ItemDyeTab/DyeCharacter.tsx new file mode 100644 index 0000000..c93d580 --- /dev/null +++ b/src/components/tab/ItemDyeTab/DyeCharacter.tsx @@ -0,0 +1,62 @@ +import type { JSX } from 'solid-js'; +import { styled } from 'styled-system/jsx/factory'; + +import type { DyeType } from '@/const/toolTab'; + +interface DyeCharacterProps { + url: string; + dyeData: Partial<Record<DyeType, number>>; + handleDyeClick: (data: Partial<Record<DyeType, number>>) => void; + ref?: (element: HTMLImageElement) => void; + dyeInfo: JSX.Element; +} +export const DyeCharacter = (props: DyeCharacterProps) => { + function handleSelect() { + props.handleDyeClick(props.dyeData); + } + function getDyeString() { + return Object.entries(props.dyeData) + .map(([key, value]) => `${key}-${value}`) + .join(', '); + } + + return ( + <CharacterItemContainer onClick={handleSelect}> + <CharacterItemImage + ref={props.ref} + src={props.url} + alt={`character-${getDyeString()}`} + title={`item-dye-${getDyeString()}`} + /> + <DyeInfoPositioner>{props.dyeInfo}</DyeInfoPositioner> + </CharacterItemContainer> + ); +}; + +const CharacterItemContainer = styled('button', { + base: { + display: 'inline-block', + position: 'relative', + _hover: { + '& [data-part="info"]': { + opacity: 0.9, + }, + }, + }, +}); + +const CharacterItemImage = styled('img', { + base: { + maxWidth: 'unset', + width: 'unset', + }, +}); + +const DyeInfoPositioner = styled('div', { + base: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + }, +}); diff --git a/src/components/tab/ItemDyeTab/DyeInfo.tsx b/src/components/tab/ItemDyeTab/DyeInfo.tsx new file mode 100644 index 0000000..19d6a88 --- /dev/null +++ b/src/components/tab/ItemDyeTab/DyeInfo.tsx @@ -0,0 +1,56 @@ +import { For } from 'solid-js'; +import { styled } from 'styled-system/jsx/factory'; +import { css } from 'styled-system/css'; + +import { Flex } from 'styled-system/jsx'; +import { DyeType } from '@/const/toolTab'; + +const GradientMap = { + [DyeType.Hue]: 'hueConic', + [DyeType.Saturation]: 'saturation', + [DyeType.Birghtness]: 'brightness', +}; + +export interface DyeInfoProps { + dyeData: Partial<Record<DyeType, number>>; +} +export const DyeInfo = (props: DyeInfoProps) => { + return ( + <DyeInfoPanel gap="1" data-part="info"> + <For each={Object.entries(props.dyeData)}> + {([key, value]) => ( + <> + <ColorBlock + class={css({ + backgroundGradient: GradientMap[key as DyeType], + })} + /> + <span>+{value}</span> + </> + )} + </For> + </DyeInfoPanel> + ); +}; + +const DyeInfoPanel = styled(Flex, { + base: { + py: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'bg.default', + borderTopRadius: 'md', + boxShadow: 'md', + opacity: 0, + transition: 'opacity 0.2s', + fontSize: 'xs', + }, +}); +const ColorBlock = styled('div', { + base: { + borderRadius: 'sm', + w: 3, + h: 3, + display: 'inline-block', + }, +}); diff --git a/src/components/tab/ItemDyeTab/DyeResult.tsx b/src/components/tab/ItemDyeTab/DyeResult.tsx new file mode 100644 index 0000000..3147971 --- /dev/null +++ b/src/components/tab/ItemDyeTab/DyeResult.tsx @@ -0,0 +1,56 @@ +import { useStore } from '@nanostores/solid'; + +import { + $isRenderingDye, + $dyeResultCount, + $dyeResultColumnCount, +} from '@/store/toolTab'; + +import { HStack } from 'styled-system/jsx/hstack'; +import { Text } from '@/components/ui/text'; +import { Heading } from '@/components/ui/heading'; +import { ResultColumnCountNumberInput } from './ResultColumnCountNumberInput'; +import { DyeResultTable } from './DyeResultTable'; +import { ExportTableButton } from './ExportTableButton'; +import { ExportSeperateButton } from '../DyeTab/ExportSeperateButton'; + +export const DyeResult = () => { + const isRenderingDye = useStore($isRenderingDye); + const count = useStore($dyeResultCount); + const columnCounts = useStore($dyeResultColumnCount); + const dyeCharacterRefs: HTMLImageElement[] = []; + + return ( + <> + <HStack> + <Heading size="lg" width="rem"> + 染色結果 + </Heading> + <HStack> + <Text>每行數量</Text> + <ResultColumnCountNumberInput /> + </HStack> + <HStack ml="auto"> + <ExportTableButton + fileName="dye-table.png" + images={dyeCharacterRefs} + imageCounts={count()} + columnCounts={columnCounts()} + disabled={isRenderingDye()} + > + 匯出表格圖 + </ExportTableButton> + <ExportSeperateButton + fileName="dye-table.zip" + images={dyeCharacterRefs} + imageCounts={count()} + disabled={isRenderingDye()} + > + 匯出(.zip) + </ExportSeperateButton> + </HStack> + </HStack> + <DyeResultTable refs={dyeCharacterRefs} /> + </> + ); +}; diff --git a/src/components/tab/ItemDyeTab/DyeResultTable.tsx b/src/components/tab/ItemDyeTab/DyeResultTable.tsx new file mode 100644 index 0000000..c550ed1 --- /dev/null +++ b/src/components/tab/ItemDyeTab/DyeResultTable.tsx @@ -0,0 +1,203 @@ +import { createEffect, For, untrack } from 'solid-js'; +import { createStore } from 'solid-js/store'; +import { useStore } from '@nanostores/solid'; + +import { + $selectedEquipSubCategory, + $dyeResultCount, + $dyeRenderId, + $isRenderingDye, + $onlyShowDyeable, + $preserveOriginalDye, + $dyeTypeEnabled, + $dyeAction, + $dyeResultColumnCount, +} from '@/store/toolTab'; +import { getEquipById } from '@/store/string'; +import { + $isGlobalRendererInitialized, + $globalRenderer, +} from '@/store/renderer'; +import { $currentCharacterInfo } from '@/store/character/store'; +import { $totalItems } from '@/store/character/selector'; +import { deepCloneCharacterItems } from '@/store/character/utils'; + +import { Character } from '@/renderer/character/character'; + +import { Grid } from 'styled-system/jsx/grid'; +import { DyeInfo } from './DyeInfo'; +import { DyeCharacter } from './DyeCharacter'; + +import { extractCanvas, getBlobFromCanvas } from '@/utils/extract'; +import { nextTick } from '@/utils/eventLoop'; + +import { DyeType } from '@/const/toolTab'; +import { CharacterExpressions } from '@/const/emotions'; +import { updateItemHsvInfo } from '@/store/character/action'; + +const DyePropertyMap = { + [DyeType.Hue]: { + min: 0, + max: 360, + }, + [DyeType.Saturation]: { + min: -100, + max: 100, + }, + [DyeType.Birghtness]: { + min: -100, + max: 100, + }, +} as const; + +function getActualNeedDyeCategories() { + const selectedEquipSubCategory = $selectedEquipSubCategory.get(); + const totalItems = $totalItems.get(); + const isOnlyShowDyeable = $onlyShowDyeable.get(); + const actaulNeedDyeCategories = selectedEquipSubCategory.filter( + (equipSubCategory) => { + /* make sure the category is in current item */ + const item = totalItems[equipSubCategory as keyof typeof totalItems]; + if (!item) { + return false; + } + if (isOnlyShowDyeable) { + return getEquipById(item.id)?.isDyeable; + } + return true; + }, + ); + return actaulNeedDyeCategories; +} + +export interface DyeResultTableProps { + refs?: HTMLImageElement[]; +} +export function DyeResultTable(props: DyeResultTableProps) { + const [state, setState] = createStore({ + results: [] as { url: string; info: Partial<Record<DyeType, number>> }[], + }); + let testIndex = 0; + const renderId = useStore($dyeRenderId); + const isInit = useStore($isGlobalRendererInitialized); + const gridColumns = useStore($dyeResultColumnCount); + const character = new Character(); + + function handleRef(i: number) { + return (element: HTMLImageElement) => { + if (!props.refs) { + return; + } + props.refs[i] = element; + }; + } + + function handleDyeClick(data: Partial<Record<DyeType, number>>) { + const actualneedDyeCategories = getActualNeedDyeCategories(); + for (const equipSubCategory of actualneedDyeCategories) { + for (const [dyeType, value] of Object.entries(data)) { + updateItemHsvInfo(equipSubCategory, dyeType as DyeType, value); + } + } + } + + function cleanUpStore() { + const currentResults = untrack(() => state.results); + for (const result of currentResults) { + URL.revokeObjectURL(result.url); + } + testIndex = 0; + setState('results', []); + } + + createEffect(async () => { + if (renderId() && isInit()) { + $isRenderingDye.set(true); + cleanUpStore(); + await nextTick(); + // return; + + const app = $globalRenderer.get(); + const currentCharacterInfo = $currentCharacterInfo.get(); + const characterData = { + frame: 0, + isAnimating: false, + action: $dyeAction.get(), + expression: CharacterExpressions.Default, + earType: currentCharacterInfo.earType, + handType: currentCharacterInfo.handType, + items: deepCloneCharacterItems($totalItems.get()), + }; + const actualneedDyeCategories = getActualNeedDyeCategories(); + const dyeResultCount = $dyeResultCount.get(); + const preserveOriginalDye = $preserveOriginalDye.get(); + const dyeTypeEnabled = $dyeTypeEnabled.get(); + if ( + !dyeTypeEnabled || + actualneedDyeCategories.length === 0 || + dyeResultCount < 1 + ) { + $isRenderingDye.set(false); + return; + } + /* reset dye when chose not preserve original dye */ + if (!preserveOriginalDye) { + for (const equipSubCategory of actualneedDyeCategories) { + if (characterData.items[equipSubCategory]) { + characterData.items[equipSubCategory][dyeTypeEnabled] = 0; + } + } + } + /* load once first */ + await character.update(characterData); + + /* start generate */ + const dyeRangeConfig = DyePropertyMap[dyeTypeEnabled]; + const step = (dyeRangeConfig.max - dyeRangeConfig.min) / dyeResultCount; + for (let i = 0; i < dyeResultCount; i++) { + const dyeNumber = Math.floor(dyeRangeConfig.min + step * i); + for (const equipSubCategory of actualneedDyeCategories) { + if (characterData.items[equipSubCategory]) { + characterData.items[equipSubCategory][dyeTypeEnabled] = dyeNumber; + } + } + await character.update(characterData); + /* give some time */ + await nextTick(); + + const canvas = extractCanvas(character, app.renderer); + const blob = await getBlobFromCanvas(canvas as HTMLCanvasElement); + if (blob) { + const url = URL.createObjectURL(blob); + setState('results', testIndex, { + url, + info: { [dyeTypeEnabled]: dyeNumber }, + }); + testIndex++; + } + } + $isRenderingDye.set(false); + } + }); + return ( + <Grid + gap={0} + justifyContent="center" + style={{ + 'grid-template-columns': `repeat(${gridColumns()}, auto)`, + }} + > + <For each={state.results}> + {(result, i) => ( + <DyeCharacter + url={result.url} + dyeData={result.info} + handleDyeClick={handleDyeClick} + ref={handleRef(i())} + dyeInfo={<DyeInfo dyeData={result.info} />} + /> + )} + </For> + </Grid> + ); +} diff --git a/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx b/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx index 9203ccc..90057f1 100644 --- a/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx +++ b/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx @@ -3,7 +3,6 @@ import { useStore } from '@nanostores/solid'; import { $dyeTypeEnabled, toggleDyeConfigEnabled } from '@/store/toolTab'; -import { HStack } from 'styled-system/jsx/hstack'; import * as RadioGroup from '@/components/ui/radioGroup'; import { DyeType } from '@/const/toolTab'; @@ -22,7 +21,6 @@ export const DyeTypeRadioGroup = () => { value={dyeTypeEnabled()} onValueChange={handleValueChange} > - {/* <HStack width="full" gap="3"> */} <RadioGroup.Item value={DyeType.Hue}> <RadioGroup.ItemControl /> <RadioGroup.ItemText> @@ -39,7 +37,7 @@ export const DyeTypeRadioGroup = () => { </RadioGroup.ItemText> <RadioGroup.ItemHiddenInput /> </RadioGroup.Item> - <RadioGroup.Item value={DyeType.Lightness}> + <RadioGroup.Item value={DyeType.Birghtness}> <RadioGroup.ItemControl /> <RadioGroup.ItemText> 亮度 @@ -47,17 +45,17 @@ export const DyeTypeRadioGroup = () => { </RadioGroup.ItemText> <RadioGroup.ItemHiddenInput /> </RadioGroup.Item> - {/* </HStack> */} </RadioGroup.Root> ); }; const ColorBlock = styled('div', { base: { - borderRadius: 'sm', + display: 'inline-block', w: 3, h: 3, - display: 'inline-block', ml: 2, + borderRadius: 'sm', + boxShadow: 'sm', }, }); diff --git a/src/components/tab/ItemDyeTab/ExportTableButton.tsx b/src/components/tab/ItemDyeTab/ExportTableButton.tsx new file mode 100644 index 0000000..628b63e --- /dev/null +++ b/src/components/tab/ItemDyeTab/ExportTableButton.tsx @@ -0,0 +1,79 @@ +import type { JSX } from 'solid-js'; + +import { Button } from '@/components/ui/button'; + +import { downloadCanvas } from '@/utils/download'; +import { toaster } from '@/components/GlobalToast'; + +const TABLE_COL_GAP = 2; +const TABLE_ROW_GAP = 2; + +export interface ExportTableButtonProps { + images: HTMLImageElement[]; + imageCounts: number; + columnCounts: number; + fileName: string; + children: JSX.Element; + disabled?: boolean; +} +export const ExportTableButton = (props: ExportTableButtonProps) => { + async function handleClick() { + const validImageCounts = props.images.filter((img) => img?.src).length; + const colCounts = props.columnCounts; + const rowCounts = Math.ceil(props.imageCounts / colCounts); + const isAllImagesLoaded = props.imageCounts === validImageCounts; + if (!isAllImagesLoaded) { + toaster.error({ + title: '圖片尚未載入完畢', + }); + return; + } + const canvas = document.createElement('canvas'); + const tableImageItem = props.images[0]; + const tableColWidth = tableImageItem.width; + + const totalWidth = (tableColWidth + TABLE_COL_GAP) * colCounts; + const totalHeight = (tableImageItem.height + TABLE_ROW_GAP) * rowCounts; + + canvas.width = totalWidth; + canvas.height = totalHeight; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + toaster.error({ + title: '匯出失敗,無法建立 Canvas', + }); + return; + } + let startY = 0; + for (let y = 0; y < rowCounts; y++) { + for (let x = 0; x < colCounts; x++) { + const index = y * colCounts + x; + const image = props.images[index]; + if (!image) { + continue; + } + ctx.drawImage(image, x * (tableColWidth + TABLE_COL_GAP), startY); + } + startY += tableImageItem.height + TABLE_ROW_GAP; + } + try { + await downloadCanvas(canvas, props.fileName); + } catch (_) { + toaster.error({ + title: '匯出失敗,Canvas 無法建立 Blob', + }); + } + } + + return ( + <Button + size="sm" + fontWeight="normal" + onClick={handleClick} + disabled={props.disabled} + > + {props.children} + </Button> + ); +}; diff --git a/src/components/tab/ItemDyeTab/NeedDyeItem.tsx b/src/components/tab/ItemDyeTab/NeedDyeItem.tsx index 50527fa..eabfcfc 100644 --- a/src/components/tab/ItemDyeTab/NeedDyeItem.tsx +++ b/src/components/tab/ItemDyeTab/NeedDyeItem.tsx @@ -1,8 +1,7 @@ -import { Show, createMemo, createEffect, splitProps } from 'solid-js'; +import { Show, createMemo, splitProps } from 'solid-js'; import { useStore } from '@nanostores/solid'; import { styled } from 'styled-system/jsx/factory'; -import type { CharacterItemInfo } from '@/store/character/store'; import { createEquipItemByCategory } from '@/store/character/selector'; import { getEquipById } from '@/store/string'; @@ -15,7 +14,6 @@ import { EquipItemName, EquipItemInfo, } from '@/components/drawer/CurrentEquipmentDrawer/EquipItem'; -import { ItemNotExistMask } from '@/components/drawer/CurrentEquipmentDrawer/ItemNotExistMask'; import type { EquipSubCategory } from '@/const/equipments'; @@ -81,7 +79,7 @@ const SelectableContainer = styled('button', { py: '1', px: '2', borderRadius: 'md', - width: 'full', + // width: 'full', gridTemplateColumns: 'auto 1fr', alignItems: 'center', position: 'relative', diff --git a/src/components/tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx b/src/components/tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx index 80ecbc1..c15e75b 100644 --- a/src/components/tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx +++ b/src/components/tab/ItemDyeTab/NeedDyeItemToggleGroup.tsx @@ -1,9 +1,10 @@ -import { For, Index } from 'solid-js'; -import { useStore } from '@nanostores/solid'; +import { Index } from 'solid-js'; -import { $onlyShowDyeable } from '@/store/toolTab'; +import { usePureStore } from '@/store'; +import { $onlyShowDyeable, $selectedEquipSubCategory } from '@/store/toolTab'; import { Grid } from 'styled-system/jsx/grid'; +import { Stack } from 'styled-system/jsx/stack'; import * as ToggleGroup from '@/components/ui/toggleGroup'; import { NeedDyeItem } from './NeedDyeItem'; @@ -26,7 +27,11 @@ const CategoryList = [ ] as EquipSubCategory[]; export const NeedDyeItemToggleGroup = () => { - const onlyShowDyeable = useStore($onlyShowDyeable); + const onlyShowDyeable = usePureStore($onlyShowDyeable); + + const handleValueChange = (details: ToggleGroup.ValueChangeDetails) => { + $selectedEquipSubCategory.set(details.value as EquipSubCategory[]); + }; return ( <ToggleGroup.Root @@ -34,8 +39,10 @@ export const NeedDyeItemToggleGroup = () => { width="full" py="0.5" borderColor="transparent" + defaultValue={$selectedEquipSubCategory.get()} + onValueChange={handleValueChange} > - <Grid width="full" columns={7}> + <Stack width="full" direction="row" flexWrap="wrap"> <Index each={CategoryList}> {(category) => ( <ToggleGroup.Item @@ -50,7 +57,7 @@ export const NeedDyeItemToggleGroup = () => { /> )} </Index> - </Grid> + </Stack> </ToggleGroup.Root> ); }; diff --git a/src/components/tab/ItemDyeTab/ResultColumnCountNumberInput.tsx b/src/components/tab/ItemDyeTab/ResultColumnCountNumberInput.tsx new file mode 100644 index 0000000..b8173c8 --- /dev/null +++ b/src/components/tab/ItemDyeTab/ResultColumnCountNumberInput.tsx @@ -0,0 +1,27 @@ +import { useStore } from '@nanostores/solid'; + +import { $dyeResultColumnCount } from '@/store/toolTab'; + +import { + NumberInput, + type ValueChangeDetails, +} from '@/components/ui/numberInput'; + +export const ResultColumnCountNumberInput = () => { + const count = useStore($dyeResultColumnCount); + + function handleCountChange(details: ValueChangeDetails) { + $dyeResultColumnCount.set(details.valueAsNumber); + } + + return ( + <NumberInput + min={2} + max={20} + value={count().toString()} + onValueChange={handleCountChange} + allowOverflow={false} + width="6rem" + /> + ); +}; diff --git a/src/components/tab/ItemDyeTab/StartDyeButton.tsx b/src/components/tab/ItemDyeTab/StartDyeButton.tsx new file mode 100644 index 0000000..f893c75 --- /dev/null +++ b/src/components/tab/ItemDyeTab/StartDyeButton.tsx @@ -0,0 +1,63 @@ +import { Show } from 'solid-js'; +import { useStore } from '@nanostores/solid'; +import { styled } from 'styled-system/jsx/factory'; + +import { + $selectedEquipSubCategory, + $dyeResultCount, + $dyeRenderId, + $dyeTypeEnabled, + $isRenderingDye, +} from '@/store/toolTab'; + +import LoaderCircle from 'lucide-solid/icons/loader-circle'; +import { Button } from '@/components/ui/button'; + +import { toaster } from '@/components/GlobalToast'; + +export const StartDyeButton = () => { + const isLoading = useStore($isRenderingDye); + + function handleClick() { + if ($isRenderingDye.get()) { + return; + } + if ($selectedEquipSubCategory.get().length === 0) { + toaster.error({ + title: '請選擇想要預覽染色的裝備', + }); + return; + } + if (!$dyeTypeEnabled.get()) { + toaster.error({ + title: '請選擇想要預覽的染色類型', + }); + } + if ($dyeResultCount.get() < 32) { + toaster.error({ + title: '請輸入染色數量', + }); + return; + } + $dyeRenderId.set(Date.now().toString()); + $isRenderingDye.set(true); + } + + return ( + <Button onClick={handleClick} disabled={isLoading()}> + <Show when={isLoading()}> + <Loading> + <LoaderCircle /> + </Loading> + </Show> + 產生染色表 + </Button> + ); +}; + +const Loading = styled('div', { + base: { + animation: 'rotate infinite 1s linear', + color: 'fg.muted', + }, +}); diff --git a/src/components/tab/ItemDyeTab/index.tsx b/src/components/tab/ItemDyeTab/index.tsx index a36ad76..17d222a 100644 --- a/src/components/tab/ItemDyeTab/index.tsx +++ b/src/components/tab/ItemDyeTab/index.tsx @@ -10,10 +10,12 @@ import { NeedDyeItemToggleGroup } from './NeedDyeItemToggleGroup'; import { DyeTypeRadioGroup } from './DyeTypeRadioGroup'; import { ResultCountNumberInput } from './ResultCountNumberInput'; import { ResultActionSelect } from './ResultActionSelect'; +import { StartDyeButton } from './StartDyeButton'; +import { DyeResult } from './DyeResult'; export const ItemDyeTab = () => { return ( - <Stack> + <Stack mb="4"> <CardContainer> <ItemDyeTabTitle /> <HStack> @@ -35,8 +37,13 @@ export const ItemDyeTab = () => { <ResultCountNumberInput /> </HStack> </HStack> + <div> + <StartDyeButton /> + </div> + </CardContainer> + <CardContainer> + <DyeResult /> </CardContainer> - <CardContainer></CardContainer> </Stack> ); }; diff --git a/src/const/toolTab.ts b/src/const/toolTab.ts index 2fac4aa..e54e8f9 100644 --- a/src/const/toolTab.ts +++ b/src/const/toolTab.ts @@ -37,5 +37,5 @@ export enum DyeOrder { export enum DyeType { Hue = 'hue', Saturation = 'saturation', - Lightness = 'lightness', + Birghtness = 'brightness', } diff --git a/src/store/toolTab.ts b/src/store/toolTab.ts index ac09063..9da128c 100644 --- a/src/store/toolTab.ts +++ b/src/store/toolTab.ts @@ -1,24 +1,24 @@ -import { atom, deepMap, computed, batched } from 'nanostores'; +import { atom, deepMap, batched, onSet } from 'nanostores'; import type { EquipSubCategory } from '@/const/equipments'; -import { - type ToolTab, - ActionExportType, - DyeOrder, - DyeType, -} from '@/const/toolTab'; +import { ToolTab, ActionExportType, DyeOrder, DyeType } from '@/const/toolTab'; import { CharacterAction } from '@/const/actions'; export const $toolTab = atom<ToolTab | undefined>(undefined); export const $actionExportType = atom<ActionExportType>(ActionExportType.Gif); +/* item dye tab */ export const $onlyShowDyeable = atom<boolean>(true); export const $preserveOriginalDye = atom<boolean>(true); export const $selectedEquipSubCategory = atom<EquipSubCategory[]>([]); export const $dyeResultCount = atom<number>(72); export const $dyeAction = atom<CharacterAction>(CharacterAction.Stand1); +export const $dyeRenderId = atom<string | undefined>(undefined); +export const $isRenderingDye = atom<boolean>(false); +export const $dyeResultColumnCount = atom<number>(8); + export interface DyeConfigOption { enabled: boolean; order: DyeOrder; @@ -26,23 +26,31 @@ export interface DyeConfigOption { export type DyeConfig = { hue: DyeConfigOption; saturation: DyeConfigOption; - lightness: DyeConfigOption; + brightness: DyeConfigOption; }; export const $dyeConfig = deepMap({ hue: { - enabled: false, + enabled: true, order: DyeOrder.Up, }, saturation: { enabled: false, order: DyeOrder.Up, }, - lightness: { + brightness: { enabled: false, order: DyeOrder.Up, }, }); +/* effect */ +onSet($toolTab, ({ newValue }) => { + /* clean render id prevent render table againg when return */ + if (newValue !== ToolTab.ItemDye) { + $dyeRenderId.set(undefined); + } +}); + /* selector */ export const $dyeTypeEnabled = batched($dyeConfig, (config) => { for (const k of Object.values(DyeType) as DyeType[]) { diff --git a/src/utils/extract.ts b/src/utils/extract.ts index c099cfc..5a4ccc8 100644 --- a/src/utils/extract.ts +++ b/src/utils/extract.ts @@ -9,6 +9,14 @@ import { type ExtractTarget = Parameters<ExtractSystem['texture']>[0]; +export function getBlobFromCanvas(canvas: HTMLCanvasElement) { + return new Promise<Blob | null>((resolve) => { + canvas.toBlob?.((blob) => { + resolve(blob as unknown as Blob); + }, 'image/png'); + }); +} + export function extractCanvas(target: ExtractTarget, renderer: Renderer) { const texture = renderer.extract.texture(target); From 4ecfb2431903f4b2dea5d11d4287f8ecd84fbb5e Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Thu, 29 Aug 2024 22:56:06 +0800 Subject: [PATCH 17/25] [edit] fix some item dye style --- src/components/tab/ItemDyeTab/DyeResult.tsx | 8 ++++---- src/components/tab/ItemDyeTab/index.tsx | 2 +- src/store/toolTab.ts | 22 ++++++++++++++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/components/tab/ItemDyeTab/DyeResult.tsx b/src/components/tab/ItemDyeTab/DyeResult.tsx index 3147971..695fc38 100644 --- a/src/components/tab/ItemDyeTab/DyeResult.tsx +++ b/src/components/tab/ItemDyeTab/DyeResult.tsx @@ -1,7 +1,7 @@ import { useStore } from '@nanostores/solid'; import { - $isRenderingDye, + $isExportable, $dyeResultCount, $dyeResultColumnCount, } from '@/store/toolTab'; @@ -15,7 +15,7 @@ import { ExportTableButton } from './ExportTableButton'; import { ExportSeperateButton } from '../DyeTab/ExportSeperateButton'; export const DyeResult = () => { - const isRenderingDye = useStore($isRenderingDye); + const isExportable = useStore($isExportable); const count = useStore($dyeResultCount); const columnCounts = useStore($dyeResultColumnCount); const dyeCharacterRefs: HTMLImageElement[] = []; @@ -36,7 +36,7 @@ export const DyeResult = () => { images={dyeCharacterRefs} imageCounts={count()} columnCounts={columnCounts()} - disabled={isRenderingDye()} + disabled={!isExportable()} > 匯出表格圖 </ExportTableButton> @@ -44,7 +44,7 @@ export const DyeResult = () => { fileName="dye-table.zip" images={dyeCharacterRefs} imageCounts={count()} - disabled={isRenderingDye()} + disabled={!isExportable()} > 匯出(.zip) </ExportSeperateButton> diff --git a/src/components/tab/ItemDyeTab/index.tsx b/src/components/tab/ItemDyeTab/index.tsx index 17d222a..1e84a9a 100644 --- a/src/components/tab/ItemDyeTab/index.tsx +++ b/src/components/tab/ItemDyeTab/index.tsx @@ -16,7 +16,7 @@ import { DyeResult } from './DyeResult'; export const ItemDyeTab = () => { return ( <Stack mb="4"> - <CardContainer> + <CardContainer gap={4}> <ItemDyeTabTitle /> <HStack> <Heading width="7rem">欲染色裝備</Heading> diff --git a/src/store/toolTab.ts b/src/store/toolTab.ts index 9da128c..7384475 100644 --- a/src/store/toolTab.ts +++ b/src/store/toolTab.ts @@ -1,12 +1,19 @@ import { atom, deepMap, batched, onSet } from 'nanostores'; import type { EquipSubCategory } from '@/const/equipments'; -import { ToolTab, ActionExportType, DyeOrder, DyeType } from '@/const/toolTab'; +import { + ToolTab, + ActionExportType, + DyeOrder, + DyeType, +} from '@/const/toolTab'; import { CharacterAction } from '@/const/actions'; export const $toolTab = atom<ToolTab | undefined>(undefined); -export const $actionExportType = atom<ActionExportType>(ActionExportType.Gif); +export const $actionExportType = atom<ActionExportType>( + ActionExportType.Gif, +); /* item dye tab */ export const $onlyShowDyeable = atom<boolean>(true); @@ -60,6 +67,12 @@ export const $dyeTypeEnabled = batched($dyeConfig, (config) => { } return undefined; }); +export const $isExportable = batched( + [$isRenderingDye, $dyeRenderId], + (isRendering, renderId) => { + return !isRendering && !!renderId; + }, +); /* action */ export function disableOtherDyeConfig(key: DyeType) { @@ -81,7 +94,10 @@ export function selectDyeCategory(category: EquipSubCategory) { if (current.includes(category)) { return; } - $selectedEquipSubCategory.set([...$selectedEquipSubCategory.get(), category]); + $selectedEquipSubCategory.set([ + ...$selectedEquipSubCategory.get(), + category, + ]); } export function deselectDyeCategory(category: EquipSubCategory) { $selectedEquipSubCategory.set( From 16a1136364ba8f35a65fdd340e8b42d9849f99bc Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Thu, 29 Aug 2024 22:57:51 +0800 Subject: [PATCH 18/25] [edit] adjust NeedDyeItem checked icon size --- src/components/tab/ItemDyeTab/NeedDyeItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/tab/ItemDyeTab/NeedDyeItem.tsx b/src/components/tab/ItemDyeTab/NeedDyeItem.tsx index eabfcfc..cf99f79 100644 --- a/src/components/tab/ItemDyeTab/NeedDyeItem.tsx +++ b/src/components/tab/ItemDyeTab/NeedDyeItem.tsx @@ -64,7 +64,7 @@ export const NeedDyeItem = (props: NeedDyeItemProps) => { </EquipItemName> </EquipItemInfo> <CheckIconContainer class="check-icon"> - <CheckIcon size="1em" /> + <CheckIcon size=".75em" /> </CheckIconContainer> </SelectableContainer> )} @@ -127,5 +127,6 @@ const CheckIconContainer = styled('div', { backgroundColor: 'accent.default', display: 'none', borderRadius: '50%', + padding: '0.5', }, }); From 5035322e2de48b5150e544df91458bab76f4f11c Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Fri, 30 Aug 2024 10:28:30 +0800 Subject: [PATCH 19/25] [edit] DyeResult should reset all dye when enable preserveOriginalDye --- src/components/tab/ItemDyeTab/DyeResultTable.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/tab/ItemDyeTab/DyeResultTable.tsx b/src/components/tab/ItemDyeTab/DyeResultTable.tsx index c550ed1..8fe6be2 100644 --- a/src/components/tab/ItemDyeTab/DyeResultTable.tsx +++ b/src/components/tab/ItemDyeTab/DyeResultTable.tsx @@ -144,7 +144,9 @@ export function DyeResultTable(props: DyeResultTableProps) { if (!preserveOriginalDye) { for (const equipSubCategory of actualneedDyeCategories) { if (characterData.items[equipSubCategory]) { - characterData.items[equipSubCategory][dyeTypeEnabled] = 0; + characterData.items[equipSubCategory].hue = 0; + characterData.items[equipSubCategory].saturation = 0; + characterData.items[equipSubCategory].brightness = 0; } } } From 4934056e7c0a3352573796de734389be89c2d503 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Fri, 30 Aug 2024 10:46:09 +0800 Subject: [PATCH 20/25] [edit] upgrade pixi.js to 8.3.4 --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index d790e12..405720b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "p-queue": "^8.0.1", "pixi-filters": "^6.0.3", "pixi-viewport": "^5.0.3", - "pixi.js": "^8.3.3", + "pixi.js": "^8.3.4", "solid-js": "^1.7.8", "throttle-debounce": "^5.0.0", "wasm-webp": "^0.0.2" @@ -5076,9 +5076,9 @@ "integrity": "sha512-DGG7cg2vUltAiL2fanzYPLR+L6qBeoskPfbUXxN6CYKW+fkni5cF9J1t2WBTmyBnC3kVq3ATFE2KDi7zy2FY8A==" }, "node_modules/pixi.js": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.3.3.tgz", - "integrity": "sha512-dpucBKAqEm0K51MQKlXvyIJ40bcxniP82uz4ZPEQejGtPp0P+vueuG5DyArHCkC48mkVE2FEDvyYvBa45/JlQg==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.3.4.tgz", + "integrity": "sha512-b5qdoHMQy79JjTiOOAH/fDiK9dLKGAoxfBwkHIdsK5XKNxsFuII2MBbktvR9pVaAmTDobDkMPDoIBFKYYpDeOg==", "dependencies": { "@pixi/colord": "^2.9.6", "@types/css-font-loading-module": "^0.0.12", @@ -9849,9 +9849,9 @@ "integrity": "sha512-DGG7cg2vUltAiL2fanzYPLR+L6qBeoskPfbUXxN6CYKW+fkni5cF9J1t2WBTmyBnC3kVq3ATFE2KDi7zy2FY8A==" }, "pixi.js": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.3.3.tgz", - "integrity": "sha512-dpucBKAqEm0K51MQKlXvyIJ40bcxniP82uz4ZPEQejGtPp0P+vueuG5DyArHCkC48mkVE2FEDvyYvBa45/JlQg==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.3.4.tgz", + "integrity": "sha512-b5qdoHMQy79JjTiOOAH/fDiK9dLKGAoxfBwkHIdsK5XKNxsFuII2MBbktvR9pVaAmTDobDkMPDoIBFKYYpDeOg==", "requires": { "@pixi/colord": "^2.9.6", "@types/css-font-loading-module": "^0.0.12", diff --git a/package.json b/package.json index 2dd20c6..0f8b5cb 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "p-queue": "^8.0.1", "pixi-filters": "^6.0.3", "pixi-viewport": "^5.0.3", - "pixi.js": "^8.3.3", + "pixi.js": "^8.3.4", "solid-js": "^1.7.8", "throttle-debounce": "^5.0.0", "wasm-webp": "^0.0.2" From 104377a81d3b7796acded4bd486c1377863c362a Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Fri, 30 Aug 2024 11:00:57 +0800 Subject: [PATCH 21/25] [edit] upgrade nanostores and @tanstack/solid-virtual --- package-lock.json | 44 ++++++++++++++++++++++---------------------- package.json | 4 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 405720b..9c664f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@ark-ui/solid": "^3.5.0", "@nanostores/solid": "^0.4.2", "@pdf-lib/upng": "^1.0.1", - "@tanstack/solid-virtual": "^3.5.1", + "@tanstack/solid-virtual": "^3.10.6", "@tauri-apps/api": ">=2.0.0-rc.0", "@tauri-apps/plugin-dialog": "^2.0.0-rc.0", "@tauri-apps/plugin-fs": "^2.0.0-rc.0", @@ -25,7 +25,7 @@ "lucide-solid": "^0.394.0", "mingcute_icon": "^2.9.4", "modern-gif": "^2.0.3", - "nanostores": "^0.10.3", + "nanostores": "^0.11.3", "p-queue": "^8.0.1", "pixi-filters": "^6.0.3", "pixi-viewport": "^5.0.3", @@ -2299,11 +2299,11 @@ } }, "node_modules/@tanstack/solid-virtual": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.5.1.tgz", - "integrity": "sha512-BhDKs3DDwGvA45rAgqeN3igcV0BJMwUX9lDxPmGDg2jEnGUy/iNeV5a8WexbbmOHzQiPNOZWirRU4C0Zc7nBnA==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.10.6.tgz", + "integrity": "sha512-r/2HiD5TOz+v0zik9DtlalPtTdYwz2HK26tKPAK/a9UUbvggVdTYbpw1IZqDLSWWh1zsp7h7FKZhmNN3r5GSJQ==", "dependencies": { - "@tanstack/virtual-core": "3.5.1" + "@tanstack/virtual-core": "3.10.6" }, "funding": { "type": "github", @@ -2314,9 +2314,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz", - "integrity": "sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.6.tgz", + "integrity": "sha512-1giLc4dzgEKLMx5pgKjL6HlG5fjZMgCjzlKAlpr7yoUtetVPELgER1NtephAI910nMwfPTHNyWKSFmJdHkz2Cw==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -4849,9 +4849,9 @@ } }, "node_modules/nanostores": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.10.3.tgz", - "integrity": "sha512-Nii8O1XqmawqSCf9o2aWqVxhKRN01+iue9/VEd1TiJCr9VT5XxgPFbF1Edl1XN6pwJcZRsl8Ki+z01yb/T/C2g==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.3.tgz", + "integrity": "sha512-TUes3xKIX33re4QzdxwZ6tdbodjmn3tWXCEc1uokiEmo14sI1EaGYNs2k3bU2pyyGNmBqFGAVl6jAGWd06AVIg==", "funding": [ { "type": "github", @@ -7701,17 +7701,17 @@ } }, "@tanstack/solid-virtual": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.5.1.tgz", - "integrity": "sha512-BhDKs3DDwGvA45rAgqeN3igcV0BJMwUX9lDxPmGDg2jEnGUy/iNeV5a8WexbbmOHzQiPNOZWirRU4C0Zc7nBnA==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.10.6.tgz", + "integrity": "sha512-r/2HiD5TOz+v0zik9DtlalPtTdYwz2HK26tKPAK/a9UUbvggVdTYbpw1IZqDLSWWh1zsp7h7FKZhmNN3r5GSJQ==", "requires": { - "@tanstack/virtual-core": "3.5.1" + "@tanstack/virtual-core": "3.10.6" } }, "@tanstack/virtual-core": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz", - "integrity": "sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ==" + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.6.tgz", + "integrity": "sha512-1giLc4dzgEKLMx5pgKjL6HlG5fjZMgCjzlKAlpr7yoUtetVPELgER1NtephAI910nMwfPTHNyWKSFmJdHkz2Cw==" }, "@tauri-apps/api": { "version": "2.0.0-rc.0", @@ -9688,9 +9688,9 @@ "dev": true }, "nanostores": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.10.3.tgz", - "integrity": "sha512-Nii8O1XqmawqSCf9o2aWqVxhKRN01+iue9/VEd1TiJCr9VT5XxgPFbF1Edl1XN6pwJcZRsl8Ki+z01yb/T/C2g==" + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.3.tgz", + "integrity": "sha512-TUes3xKIX33re4QzdxwZ6tdbodjmn3tWXCEc1uokiEmo14sI1EaGYNs2k3bU2pyyGNmBqFGAVl6jAGWd06AVIg==" }, "node-eval": { "version": "2.0.0", diff --git a/package.json b/package.json index 0f8b5cb..84031fa 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@ark-ui/solid": "^3.5.0", "@nanostores/solid": "^0.4.2", "@pdf-lib/upng": "^1.0.1", - "@tanstack/solid-virtual": "^3.5.1", + "@tanstack/solid-virtual": "^3.10.6", "@tauri-apps/api": ">=2.0.0-rc.0", "@tauri-apps/plugin-dialog": "^2.0.0-rc.0", "@tauri-apps/plugin-fs": "^2.0.0-rc.0", @@ -30,7 +30,7 @@ "lucide-solid": "^0.394.0", "mingcute_icon": "^2.9.4", "modern-gif": "^2.0.3", - "nanostores": "^0.10.3", + "nanostores": "^0.11.3", "p-queue": "^8.0.1", "pixi-filters": "^6.0.3", "pixi-viewport": "^5.0.3", From 2ce2d42e8afe7ac92c69839f5e54bc6b7a1001bd Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Fri, 30 Aug 2024 13:09:17 +0800 Subject: [PATCH 22/25] [edit] fix SceneColorPicker will be block by sidebar --- src/components/AppContainer.tsx | 2 +- src/components/CharacterPreview/CharacterSceneColorPicker.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AppContainer.tsx b/src/components/AppContainer.tsx index e8e3a45..1fb675a 100644 --- a/src/components/AppContainer.tsx +++ b/src/components/AppContainer.tsx @@ -18,7 +18,7 @@ export const AppContainer = (props: AppContainerProps) => { class={css({ position: 'relative', mx: { base: 0, lg: 2 }, - mt: 4, + mt: 11, paddingLeft: isLeftDrawerPin() ? { base: 2, lg: '{sizes.xs}' } : { base: 2, '2xl': '{sizes.xs}' }, diff --git a/src/components/CharacterPreview/CharacterSceneColorPicker.tsx b/src/components/CharacterPreview/CharacterSceneColorPicker.tsx index abf1333..cf13b16 100644 --- a/src/components/CharacterPreview/CharacterSceneColorPicker.tsx +++ b/src/components/CharacterPreview/CharacterSceneColorPicker.tsx @@ -59,6 +59,7 @@ export const CharacterSceneColorPicker = ( defaultValue="#ffffff" positioning={{ strategy: 'fixed', + placement: 'top-end' }} onInteractOutside={handleOutsideClick} onValueChange={handleColorChange} From 1dd7fe3a2b3ee1a3c9e4572bf6541a986ff30035 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Fri, 30 Aug 2024 13:14:18 +0800 Subject: [PATCH 23/25] [edit] EquipDrawer should lock close when get pinned --- src/components/drawer/EqupimentDrawer/EquipDrawer.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/drawer/EqupimentDrawer/EquipDrawer.tsx b/src/components/drawer/EqupimentDrawer/EquipDrawer.tsx index 9b0ba07..59fdae8 100644 --- a/src/components/drawer/EqupimentDrawer/EquipDrawer.tsx +++ b/src/components/drawer/EqupimentDrawer/EquipDrawer.tsx @@ -22,8 +22,12 @@ interface EquipDrawerProps { } export const EquipDrawer = (props: EquipDrawerProps) => { const isOpen = useStore($equpimentDrawerOpen); + const isPinned = useStore($equpimentDrawerPin); function handleClose(_: unknown) { + if (isPinned()) { + return; + } $equpimentDrawerOpen.set(false); } @@ -44,7 +48,11 @@ export const EquipDrawer = (props: EquipDrawerProps) => { {props.header} <HStack position="absolute" top="1" right="1"> <PinIconButton store={$equpimentDrawerPin} variant="ghost" /> - <IconButton variant="ghost" onClick={handleClose}> + <IconButton + variant="ghost" + onClick={handleClose} + disabled={isPinned()} + > <CloseIcon /> </IconButton> </HStack> From f4ea4ca33673fc2dc8086b425864f796ff9b3c26 Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Fri, 30 Aug 2024 13:18:37 +0800 Subject: [PATCH 24/25] [edit] adjust NeedDyeItem style --- src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx | 2 +- src/components/tab/ItemDyeTab/NeedDyeItem.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx b/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx index 90057f1..d00450a 100644 --- a/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx +++ b/src/components/tab/ItemDyeTab/DyeTypeRadioGroup.tsx @@ -56,6 +56,6 @@ const ColorBlock = styled('div', { h: 3, ml: 2, borderRadius: 'sm', - boxShadow: 'sm', + boxShadow: 'md', }, }); diff --git a/src/components/tab/ItemDyeTab/NeedDyeItem.tsx b/src/components/tab/ItemDyeTab/NeedDyeItem.tsx index cf99f79..dbccf99 100644 --- a/src/components/tab/ItemDyeTab/NeedDyeItem.tsx +++ b/src/components/tab/ItemDyeTab/NeedDyeItem.tsx @@ -77,8 +77,10 @@ const SelectableContainer = styled('button', { base: { display: 'grid', py: '1', - px: '2', + pl: '1', + pr: '2', borderRadius: 'md', + gap: '1', // width: 'full', gridTemplateColumns: 'auto 1fr', alignItems: 'center', From afbaf8145ed118bf30c3697c2ee5d66514eb18ad Mon Sep 17 00:00:00 2001 From: spd789562 <leo.yicun.lin@gmail.com> Date: Fri, 30 Aug 2024 13:29:54 +0800 Subject: [PATCH 25/25] [release] v0.2.0 --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 961b8a2..68efcaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,20 @@ +## [0.2.0] +還是測試版,目前還在想要新增什麼功能 + +### 新增 +- 設定新增開起存檔及暫存資料夾的按鈕,以便使用者備份或移機 +- 設定新增版本顯示 +- 裝備染色表格,預覽當前裝備色相、飽和或亮度的變化 +- 高清化(Anime4K) 現在將會直接套用在預覽角色身上 + +### 修改 +- 修正皇家神獸學院裝備無法載入的問題(no slot/vslot at info) +- 修正啟用篩選染色時會導致髮型及臉型也被套用的問題 +- 修正一些鞋子渲染錯誤 +- 修正部分披風渲染時會有部件消失的問題 +- 小修改一些 UI +- 升級一些套件 + + ## [0.1.0] - 首次發布測試版 \ No newline at end of file diff --git a/package.json b/package.json index 84031fa..51be7bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maplecharactercreator", - "version": "0.0.0", + "version": "0.2.0", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3a30426..fbc22e6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2158,7 +2158,7 @@ dependencies = [ [[package]] name = "maplesalon2" -version = "0.1.0" +version = "0.2.0" dependencies = [ "axum", "futures", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index df4b2ec..a6ad1bf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maplesalon2" -version = "0.1.0" +version = "0.2.0" description = "MapleSalon2 - A tool for preview hair, face and item dye using MapleStory wz files." authors = ["Leo"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1e3d567..2ca901b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "MapleSalon2", - "version": "0.1.0", + "version": "0.2.0", "identifier": "com.maplesalon.io", "build": { "beforeDevCommand": "npm run dev",