@@ -13,6 +13,8 @@ import {
1313 ArrowsOutIcon ,
1414 PushPinIcon ,
1515 FloppyDiskBackIcon ,
16+ PlusIcon ,
17+ SquareIcon ,
1618} from '@phosphor-icons/react' ;
1719import useSeo from '../../hooks/useSeo' ;
1820import { useToast } from '../../hooks/useToast' ;
@@ -94,11 +96,36 @@ const MagazinerPage = () => {
9496 const [ shapesCount , setShapesCount ] = useState ( 15 ) ;
9597 const [ borderWidth , setBorderWidth ] = useState ( 10 ) ;
9698 const [ inputs , setInputs ] = useState ( initialInputs ) ;
99+ const [ assets , setAssets ] = useState ( [ ] ) ;
97100 const [ isSaveDialogOpen , setIsSaveDialogOpen ] = useState ( false ) ;
98101 const [ isLoadDialogOpen , setIsLoadDialogOpen ] = useState ( false ) ;
99102 const [ isExportDialogOpen , setIsExportDialogOpen ] = useState ( false ) ;
100103 const [ stickyPreview , setStickyPreview ] = useState ( true ) ;
101104
105+ const addAsset = ( type ) => {
106+ const newAsset = {
107+ id : Date . now ( ) ,
108+ type,
109+ x : 50 ,
110+ y : 50 ,
111+ width : type === 'line' ? 20 : 10 ,
112+ height : type === 'line' ? 0.5 : 10 ,
113+ rotation : 0 ,
114+ opacity : 1 ,
115+ } ;
116+ setAssets ( [ ...assets , newAsset ] ) ;
117+ addToast ( { title : 'ASSET_ADDED' , message : `New ${ type . toUpperCase ( ) } entity initialized.` } ) ;
118+ } ;
119+
120+ const updateAsset = ( id , field , value ) => {
121+ setAssets ( assets . map ( a => a . id === id ? { ...a , [ field ] : value } : a ) ) ;
122+ } ;
123+
124+ const removeAsset = ( id ) => {
125+ setAssets ( assets . filter ( a => a . id !== id ) ) ;
126+ addToast ( { title : 'ASSET_REMOVED' , message : 'Entity purged from current sequence.' , type : 'info' } ) ;
127+ } ;
128+
102129 const handleSavePreset = ( ) => {
103130 const preset = {
104131 style,
@@ -111,6 +138,7 @@ const MagazinerPage = () => {
111138 shapesCount,
112139 borderWidth,
113140 inputs,
141+ assets,
114142 } ;
115143 localStorage . setItem ( 'magaziner_preset' , JSON . stringify ( preset ) ) ;
116144 addToast ( { title : 'PRESET_SAVED' , message : 'Current configuration stored in local memory.' } ) ;
@@ -131,6 +159,7 @@ const MagazinerPage = () => {
131159 setShapesCount ( preset . shapesCount ) ;
132160 setBorderWidth ( preset . borderWidth ) ;
133161 setInputs ( preset . inputs ) ;
162+ if ( preset . assets ) setAssets ( preset . assets ) ;
134163 addToast ( { title : 'PRESET_LOADED' , message : 'Configuration successfully restored.' } ) ;
135164 } catch ( e ) {
136165 addToast ( { title : 'LOAD_ERROR' , message : 'Stored preset is corrupted or incompatible.' , type : 'error' } ) ;
@@ -444,7 +473,32 @@ const MagazinerPage = () => {
444473 }
445474 ctx . restore ( ) ;
446475
447- // 4. Typography
476+ // 4. Manual Assets (Structural Entities)
477+ ctx . save ( ) ;
478+ assets . forEach ( asset => {
479+ ctx . save ( ) ;
480+ const ax = ( asset . x / 100 ) * width ;
481+ const ay = ( asset . y / 100 ) * height ;
482+ const aw = ( asset . width / 100 ) * width ;
483+ const ah = ( asset . height / 100 ) * height ;
484+
485+ ctx . translate ( ax , ay ) ;
486+ ctx . rotate ( asset . rotation * ( Math . PI / 180 ) ) ;
487+ ctx . globalAlpha = asset . opacity ;
488+ ctx . fillStyle = accentColor . hex ;
489+ ctx . strokeStyle = accentColor . hex ;
490+ ctx . lineWidth = 1 * scale ;
491+
492+ if ( asset . type === 'line' ) {
493+ ctx . fillRect ( - aw / 2 , - ah / 2 , aw , ah ) ;
494+ } else if ( asset . type === 'box' ) {
495+ ctx . strokeRect ( - aw / 2 , - ah / 2 , aw , ah ) ;
496+ }
497+ ctx . restore ( ) ;
498+ } ) ;
499+ ctx . restore ( ) ;
500+
501+ // 5. Typography
448502 if ( options . includeText ) {
449503 ctx . fillStyle = accentColor . hex ;
450504 ctx . textBaseline = 'middle' ;
@@ -518,7 +572,7 @@ const MagazinerPage = () => {
518572 ctx . fillRect ( 0 , 0 , width , height ) ;
519573 ctx . restore ( ) ;
520574 }
521- } , [ style , pattern , primaryColor , accentColor , bgImage , seed , shapesCount , shapesOpacity , noiseOpacity , gridOpacity , borderWidth , inputs ] ) ;
575+ } , [ style , pattern , primaryColor , accentColor , bgImage , seed , shapesCount , shapesOpacity , noiseOpacity , gridOpacity , borderWidth , inputs , assets ] ) ;
522576
523577 useEffect ( ( ) => {
524578 const canvas = canvasRef . current ;
@@ -689,6 +743,92 @@ const MagazinerPage = () => {
689743 </ div >
690744 </ div >
691745
746+ < div className = "border border-white/10 bg-white/[0.02] p-8 rounded-sm space-y-10" >
747+ < h3 className = "font-mono text-[10px] font-bold text-emerald-500 uppercase tracking-widest flex items-center gap-2 border-b border-white/5 pb-6" >
748+ < SquareIcon weight = "fill" />
749+ Visual_Assets_Manager
750+ </ h3 >
751+
752+ < div className = "space-y-8" >
753+ < div className = "grid grid-cols-2 gap-4" >
754+ < button
755+ onClick = { ( ) => addAsset ( 'line' ) }
756+ className = "py-3 border border-white/10 text-[9px] font-mono uppercase tracking-widest hover:bg-emerald-500 hover:text-black transition-all flex items-center justify-center gap-2"
757+ >
758+ < PlusIcon weight = "bold" /> Add_Line
759+ </ button >
760+ < button
761+ onClick = { ( ) => addAsset ( 'box' ) }
762+ className = "py-3 border border-white/10 text-[9px] font-mono uppercase tracking-widest hover:bg-emerald-500 hover:text-black transition-all flex items-center justify-center gap-2"
763+ >
764+ < PlusIcon weight = "bold" /> Add_Box
765+ </ button >
766+ </ div >
767+
768+ < div className = "space-y-12 overflow-y-auto max-h-[400px] pr-2 custom-scrollbar" >
769+ { assets . map ( ( asset ) => (
770+ < div key = { asset . id } className = "p-4 border border-white/5 bg-white/[0.01] space-y-6" >
771+ < div className = "flex justify-between items-center border-b border-white/5 pb-2" >
772+ < span className = "text-[9px] font-mono text-gray-500 uppercase" > { asset . type } { '//' } { asset . id . toString ( ) . slice ( - 4 ) } </ span >
773+ < button
774+ onClick = { ( ) => removeAsset ( asset . id ) }
775+ className = "text-red-500 hover:text-white transition-colors"
776+ >
777+ < TrashIcon weight = "bold" size = { 14 } />
778+ </ button >
779+ </ div >
780+
781+ < div className = "space-y-4" >
782+ < CustomSlider
783+ label = "X Position"
784+ min = { 0 }
785+ max = { 100 }
786+ value = { asset . x }
787+ onChange = { ( val ) => updateAsset ( asset . id , 'x' , val ) }
788+ />
789+ < CustomSlider
790+ label = "Y Position"
791+ min = { 0 }
792+ max = { 100 }
793+ value = { asset . y }
794+ onChange = { ( val ) => updateAsset ( asset . id , 'y' , val ) }
795+ />
796+ < CustomSlider
797+ label = "Width"
798+ min = { 0.1 }
799+ max = { 100 }
800+ value = { asset . width }
801+ onChange = { ( val ) => updateAsset ( asset . id , 'width' , val ) }
802+ />
803+ < CustomSlider
804+ label = "Height / Thickness"
805+ min = { 0.1 }
806+ max = { 100 }
807+ value = { asset . height }
808+ onChange = { ( val ) => updateAsset ( asset . id , 'height' , val ) }
809+ />
810+ < CustomSlider
811+ label = "Rotation"
812+ min = { 0 }
813+ max = { 360 }
814+ value = { asset . rotation }
815+ onChange = { ( val ) => updateAsset ( asset . id , 'rotation' , val ) }
816+ />
817+ < CustomSlider
818+ label = "Opacity"
819+ min = { 0 }
820+ max = { 1 }
821+ step = { 0.01 }
822+ value = { asset . opacity }
823+ onChange = { ( val ) => updateAsset ( asset . id , 'opacity' , val ) }
824+ />
825+ </ div >
826+ </ div >
827+ ) ) }
828+ </ div >
829+ </ div >
830+ </ div >
831+
692832 { Object . entries ( inputs ) . map ( ( [ key , config ] ) => (
693833 < div key = { key } className = "border border-white/10 bg-white/[0.02] p-8 rounded-sm space-y-10" >
694834 < h3 className = "font-mono text-[10px] font-bold text-emerald-500 uppercase tracking-widest flex items-center gap-2 border-b border-white/5 pb-6" >
0 commit comments