1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./browserAll-DiLbNwOz.js","./webworkerAll-acwYx1_h.js","./colorToUniform-BD1KgYAg.js","./i18n-dpAHICcw.js","./index-CDZuCcOm.js","./index-DYz7DaJH.css","./i18n-UT_CQGRO.css","./misc-C2MjMwBX.js","./utils.svelte-CyWLYi-B.js","./clone-dZfS06Ds.js","./ImagePreview-BoKQyUgY.js","./ScrollFade.svelte_svelte_type_style_lang-DUvCSfRk.js","./snippet-DVkMfmSq.js","./ScrollFade-DXC81vHi.css","./MarkdownCode.svelte_svelte_type_style_lang-GeNUGzep.js","./prism-python-C_fanlsZ.js","./MarkdownCode-Dptk5bYK.css","./BlockLabel-D4yjUUAn.js","./DownloadLink-CmpyjDGR.js","./Empty-617iGDfy.js","./ShareButton-c5E0ChlI.js","./Download-bn1Yc3QR.js","./Image-CdSr17SW.js","./IconButtonWrapper-KjCt2Pl8.js","./FullscreenButton-wNz2x9hr.js","./Maximize-CNFXHhlb.js","./Image-CJziNDBt.js","./Image-DQfoGqtb.css","./ImagePreview-CAbFrrhc.css","./Block-DntE23uJ.js","./index-DyDpuTN9.js","./StreamingBar.svelte_svelte_type_style_lang-BxBb9ZZb.js","./html-h_YSgefI.js","./StreamingBar-M0OJiclA.css","./Clear-tvJMRS4J.js","./Upload-BlCAnBIo.js","./actions-BTh6ZJJ8.js","./Upload-B9AbOy1B.css","./props-BwDqDG8n.js","./Webcam-D1D2o0z2.js","./DropdownArrow-BRSpwupS.js","./Square-Bg2evxzG.js","./Spinner-CNWYfN22.js","./StreamingBar-BnG6Vbta.js","./ImageUploader-79Z_jqCu.css","./window-DwfrWsjF.js","./tinycolor-DVeHTKjv.js","./Plus-CQgnmcGN.js","./input-UUW65DyE.js","./Check-4kogBHUX.js","./event-modifiers-DanhKw3_.js","./ArrowUp-BZnStkIc.js","./Trash-DD3UIc2i.js","./Undo-BTdg4xEQ.js","./Example-BDoLBXEz.css","./WebGPURenderer-C0Fiuo6s.js","./SharedSystems-CHymZ9tt.js","./WebGLRenderer-x_6nJWeO.js"])))=>i.map(i=>d[i]); 2import { w as writable, p as prop, e as init$1, i as if_block, a as set_attribute, b as set_class, f as set_style, G as getDefaultExportFromCjs, d as bind_this, k as each, u as index, t as remove_input_defaults, F as set_value, j as get$1, m as setup_stores, q as store_get, s as slot, r as rest_props, g as spread_props } from './i18n-dpAHICcw.js'; 3import { a as append, f as from_svg, ak as delegate, R as push, a1 as legacy_pre_effect, x as set, I as deep_read_state, a2 as legacy_pre_effect_reset, V as child, X as sibling, Y as reset, t as template_effect, w as get, T as pop, a3 as mutable_source, U as flushSync, a0 as set_text, W as from_html, ag as __vitePreload, ab as onMount, Z as event, ax as $window, S as first_child, z as untrack, N as tick, a4 as createEventDispatcher, ae as derived_safe_equal, a5 as user_derived, au as to_array, a6 as comment, av as prepare_files, u as state, aw as FileData } from './index-CDZuCcOm.js'; 4import { b as bubble_event } from './misc-C2MjMwBX.js'; 5import { G as Gradio } from './utils.svelte-CyWLYi-B.js'; 6import { I as ImagePreview } from './ImagePreview-BoKQyUgY.js'; 7import { B as Block } from './Block-DntE23uJ.js'; 8import './MarkdownCode.svelte_svelte_type_style_lang-GeNUGzep.js'; 9import { U as Upload, a as ImagePaste, W as Webcam, I as IconButton$1 } from './ScrollFade.svelte_svelte_type_style_lang-DUvCSfRk.js'; 10import { s as spring, S as Static } from './index-DyDpuTN9.js'; 11import './StreamingBar.svelte_svelte_type_style_lang-BxBb9ZZb.js'; 12/* empty css */ 13import { c as create_drag } from './Upload-BlCAnBIo.js'; 14/* empty css */ 15/* empty css */ 16import { b as bind_prop } from './props-BwDqDG8n.js'; 17import { a as action } from './actions-BTh6ZJJ8.js'; 18import { C as Circle$1, W as Webcam$1 } from './Webcam-D1D2o0z2.js'; 19import { I as Image$1 } from './Image-CdSr17SW.js'; 20import { b as bind_window_size } from './window-DwfrWsjF.js'; 21import { t as tinycolor, E as Eyedropper } from './tinycolor-DVeHTKjv.js'; 22import { P as Plus } from './Plus-CQgnmcGN.js'; 23import { b as bind_value } from './input-UUW65DyE.js'; 24import { C as Check } from './Check-4kogBHUX.js'; 25/* empty css */ 26import { s as stopPropagation } from './event-modifiers-DanhKw3_.js'; 27import { A as ArrowUp } from './ArrowUp-BZnStkIc.js'; 28import { C as Clear } from './Clear-tvJMRS4J.js'; 29import { D as Download } from './Download-bn1Yc3QR.js'; 30import { T as Trash }
30 from './Trash-DD3UIc2i.js'; 31import { U as Undo } from './Undo-BTdg4xEQ.js'; 32import { I as IconButtonWrapper } from './IconButtonWrapper-KjCt2Pl8.js'; 33import { B as BlockLabel } from './BlockLabel-D4yjUUAn.js'; 34 35var root$l = from_svg(`<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>`); 36 37function ArrowDown($$anchor) { 38 var svg = root$l(); 39 40 append($$anchor, svg); 41} 42 43var root$k = from_svg(`<svg width="100%" height="100%" viewBox="0 0 32 32"><path d="M28.828 3.172a4.094 4.094 0 0 0-5.656 0L4.05 22.292A6.954 6.954 0 0 0 2 27.242V30h2.756a6.952 6.952 0 0 0 4.95-2.05L28.828 8.829a3.999 3.999 0 0 0 0-5.657zM10.91 18.26l2.829 2.829l-2.122 2.121l-2.828-2.828zm-2.619 8.276A4.966 4.966 0 0 1 4.756 28H4v-.759a4.967 4.967 0 0 1 1.464-3.535l1.91-1.91l2.829 2.828zM27.415 7.414l-12.261 12.26l-2.829-2.828l12.262-12.26a2.047 2.047 0 0 1 2.828 0a2 2 0 0 1 0 2.828z" fill="currentColor"></path><path d="M6.5 15a3.5 3.5 0 0 1-2.475-5.974l3.5-3.5a1.502 1.502 0 0 0 0-2.121a1.537 1.537 0 0 0-2.121 0L3.415 5.394L2 3.98l1.99-1.988a3.585 3.585 0 0 1 4.95 0a3.504 3.504 0 0 1 0 4.949L5.439 10.44a1.502 1.502 0 0 0 0 2.121a1.537 1.537 0 0 0 2.122 0l4.024-4.024L13 9.95l-4.025 4.024A3.475 3.475 0 0 1 6.5 15z" fill="currentColor"></path></svg>`); 44 45function Brush($$anchor) { 46 var svg = root$k(); 47 48 append($$anchor, svg); 49} 50 51var root$j = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"><path fill="currentColor" d="M2.753 2.933a.75.75 0 0 1 .814-.68l3.043.272c2.157.205 4.224.452 5.922.732c1.66.273 3.073.594 3.844.983c.197.1.412.233.578.415c.176.192.352.506.28.9c-.067.356-.304.59-.487.729a3.001 3.001 0 0 1-.695.369c-1.02.404-2.952.79-5.984 1.169c-1.442.18-2.489.357-3.214.522c.205.045.43.089.674.132c.992.174 2.241.323 3.568.437a31.21 31.21 0 0 1 3.016.398c.46.087.893.186 1.261.296c.352.105.707.236.971.412c.13.086.304.225.42.437a.988.988 0 0 1 .063.141A1.75 1.75 0 0 0 14.5 12.25v.158c-.758.154-1.743.302-2.986.444c-2.124.243-3.409.55-4.117.859c-.296.128-.442.236-.508.3c.026.037.073.094.156.17c.15.138.369.29.65.45c.56.316 1.282.61 1.979.838l2.637.814a.75.75 0 1 1-.443 1.433l-2.655-.819c-.754-.247-1.58-.578-2.257-.96a5.082 5.082 0 0 1-.924-.65c-.255-.233-.513-.544-.62-.935c-.12-.441-.016-.88.274-1.244c.261-.328.656-.574 1.113-.773c.92-.4 2.387-.727 4.545-.974c1.366-.156 2.354-.313 3.041-.462a16.007 16.007 0 0 0-.552-.114a29.716 29.716 0 0 0-2.865-.378c-1.352-.116-2.649-.27-3.7-.454c-.524-.092-1-.194-1.395-.307c-.376-.106-.75-.241-1.021-.426a1.186 1.186 0 0 1-.43-.49a.934.934 0 0 1 .059-.873c.13-.213.32-.352.472-.442a3.23 3.23 0 0 1 .559-.251c.807-.287 2.222-.562 4.37-.83c2.695-.338 4.377-.666 5.295-.962c-.638-.21-1.623-.427-2.89-.635c-1.65-.273-3.679-.515-5.816-.718l-3.038-.272a.75.75 0 0 1-.68-.814M17 12.25a.75.75 0 0 0-1.5 0v4.19l-.72-.72a.75.75 0 1 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l2-2a.75.75 0 1 0-1.06-1.06l-.72.72z"></path></svg>`); 52 53function BrushSize($$anchor) { 54 var svg = root$j(); 55 56 append($$anchor, svg); 57} 58 59var root$i = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22q-2.05 0-3.875-.788t-3.187-2.15t-2.15-3.187T2 12q0-2.075.813-3.9t2.2-3.175T8.25 2.788T12.2 2q2 0 3.775.688t3.113 1.9t2.125 2.875T22 11.05q0 2.875-1.75 4.413T16 17h-1.85q-.225 0-.312.125t-.088.275q0 .3.375.863t.375 1.287q0 1.25-.687 1.85T12 22m-5.5-9q.65 0 1.075-.425T8 11.5t-.425-1.075T6.5 10t-1.075.425T5 11.5t.425 1.075T6.5 13m3-4q.65 0 1.075-.425T11 7.5t-.425-1.075T9.5 6t-1.075.425T8 7.5t.425 1.075T9.5 9m5 0q.65 0 1.075-.425T16 7.5t-.425-1.075T14.5 6t-1.075.425T13 7.5t.425 1.075T14.5 9m3 4q.65 0 1.075-.425T19 11.5t-.425-1.075T17.5 10t-1.075.425T16 11.5t.425 1.075T17.5 13"></path></svg>`); 60 61function ColorPickerSolid($$anchor) { 62 var svg = root$i(); 63 64 append($$anchor, svg); 65} 66 67var root$h = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewB
67ox="0 0 256 256"><path fill="currentColor" d="M240 192a8 8 0 0 1-8 8h-32v32a8 8 0 0 1-16 0v-32H64a8 8 0 0 1-8-8V72H24a8 8 0 0 1 0-16h32V24a8 8 0 0 1 16 0v160h160a8 8 0 0 1 8 8M96 72h88v88a8 8 0 0 0 16 0V64a8 8 0 0 0-8-8H96a8 8 0 0 0 0 16"></path></svg>`); 68 69function Crop($$anchor) { 70 var svg = root$h(); 71 72 append($$anchor, svg); 73} 74 75var root$g = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"><g fill="none"><path fill="currentColor" d="m5.505 11.41l.53.53l-.53-.53ZM3 14.952h-.75H3ZM9.048 21v.75V21ZM11.41 5.505l-.53-.53l.53.53Zm1.831 12.34a.75.75 0 0 0 1.06-1.061l-1.06 1.06ZM7.216 9.697a.75.75 0 1 0-1.06 1.061l1.06-1.06Zm10.749 2.362l-5.905 5.905l1.06 1.06l5.905-5.904l-1.06-1.06Zm-11.93-.12l5.905-5.905l-1.06-1.06l-5.905 5.904l1.06 1.06Zm0 6.025c-.85-.85-1.433-1.436-1.812-1.933c-.367-.481-.473-.79-.473-1.08h-1.5c0 .749.312 1.375.78 1.99c.455.596 1.125 1.263 1.945 2.083l1.06-1.06Zm-1.06-7.086c-.82.82-1.49 1.488-1.945 2.084c-.468.614-.78 1.24-.78 1.99h1.5c0-.29.106-.6.473-1.08c.38-.498.962-1.083 1.812-1.933l-1.06-1.06Zm7.085 7.086c-.85.85-1.435 1.433-1.933 1.813c-.48.366-.79.472-1.08.472v1.5c.75 0 1.376-.312 1.99-.78c.596-.455 1.264-1.125 2.084-1.945l-1.06-1.06Zm-7.085 1.06c.82.82 1.487 1.49 2.084 1.945c.614.468 1.24.78 1.989.78v-1.5c-.29 0-.599-.106-1.08-.473c-.497-.38-1.083-.962-1.933-1.812l-1.06 1.06Zm12.99-12.99c.85.85 1.433 1.436 1.813 1.933c.366.481.472.79.472 1.08h1.5c0-.749-.312-1.375-.78-1.99c-.455-.596-1.125-1.263-1.945-2.083l-1.06 1.06Zm1.06 7.086c.82-.82 1.49-1.488 1.945-2.084c.468-.614.78-1.24.78-1.99h-1.5c0 .29-.106.6-.473 1.08c-.38.498-.962 1.083-1.812 1.933l1.06 1.06Zm0-8.146c-.82-.82-1.487-1.49-2.084-1.945c-.614-.468-1.24-.78-1.989-.78v1.5c.29 0 .599.106 1.08.473c.497.38 1.083.962 1.933 1.812l1.06-1.06Zm-7.085 1.06c.85-.85 1.435-1.433 1.933-1.812c.48-.367.79-.473 1.08-.473v-1.5c-.75 0-1.376.312-1.99.78c-.596.455-1.264 1.125-2.084 1.945l1.06 1.06Zm2.362 10.749L7.216 9.698l-1.06 1.061l7.085 7.085l1.06-1.06Z"></path><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M9 21h12"></path></g></svg>`); 76 77function Erase($$anchor) { 78 var svg = root$g(); 79 80 append($$anchor, svg); 81} 82 83var root$f = from_svg(`<svg width="100%" height="100%" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.35327 10.9495L6.77663 15.158C7.12221 15.4229 7.50051 15.5553 7.91154 15.5553C8.32258 15.5553 8.70126 15.4229 9.0476 15.158L14.471 10.9495" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><path d="M7.23461 11.4324C7.23406 11.432 7.2335 11.4316 7.23295 11.4312L1.81496 7.2268C1.81471 7.22661 1.81446 7.22641 1.8142 7.22621C1.52269 6.99826 1.39429 6.73321 1.39429 6.37014C1.39429 6.00782 1.52236 5.74301 1.81325 5.51507C1.8136 5.5148 1.81394 5.51453 1.81428 5.51426L7.2331 1.30812C7.45645 1.13785 7.67632 1.06653 7.91159 1.06653C8.14692 1.06653 8.36622 1.13787 8.58861 1.30787C8.58915 1.30828 8.58969 1.30869 8.59023 1.30911L14.0082 5.51462C14.0085 5.51485 14.0088 5.51507 14.0091 5.51529C14.3008 5.74345 14.4289 6.00823 14.4289 6.37014C14.4289 6.73356 14.3006 6.99862 14.01 7.22634C14.0096 7.22662 14.0093 7.22689 14.0089 7.22717L8.59007 11.4322C8.36672 11.6024 8.14686 11.6738 7.91159 11.6738C7.67628 11.6738 7.45699 11.6024 7.23461 11.4324Z" stroke="currentColor" stroke-width="1.5"></path></svg>`); 84 85function Layers($$anchor) { 86 var svg = root$f(); 87 88 append($$anchor, svg); 89} 90 91var root$e = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"><path fill="currentColor" d="M10.05 23q-.75 0-1.4-.337T7.575 21.7l-5.9-8.65q-.2-.3-.175-.65t.3-.6q.475-.475 1.125-.55t1.175.3L7 13.575V4q0-.425.288-.712T8 3t.713.288T9 4v11.5q0 .6-.537.888t-1.038-.063l-2.125-1.5l3.925 5.725q.125.2.35.325t.475.125H17q.825 0 1.413-.587T19 19V5q0-.425.288-.712T20 4t.713.288T21 5v14q0 1.65-1.175 2.825T17 23zM12 1q.425 0 .713.288T13 2v9q0 .425-.288.713T12 12t-.712-.288T11 11V2q0-.425.288-.712T12 1m4 1q.425 0 .713.288T17 3v8q0 .425-.288.713T16 12t-.712-.288T15 11V3q0-.425.288-.712T16 2m-3.85 14.5"></path></svg>`); 92 93function Pan($$anchor) { 94 var svg = root$e(); 95 96 append($$anchor, svg); 97} 98 99var root$d = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewB
99ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-ccw" style="transform: rotateY(180deg);"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>`); 100 101function Redo($$anchor) { 102 var svg = root$d(); 103 104 append($$anchor, svg); 105} 106 107var root$c = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"><path fill="currentColor" d="M12 16q1.875 0 3.188-1.312T16.5 11.5t-1.312-3.187T12 7T8.813 8.313T7.5 11.5t1.313 3.188T12 16m0-1.8q-1.125 0-1.912-.788T9.3 11.5t.788-1.912T12 8.8t1.913.788t.787 1.912t-.787 1.913T12 14.2m0 4.8q-3.65 0-6.65-2.037T1 11.5q1.35-3.425 4.35-5.462T12 4t6.65 2.038T23 11.5q-1.35 3.425-4.35 5.463T12 19"></path></svg>`); 108 109function Visibility($$anchor) { 110 var svg = root$c(); 111 112 append($$anchor, svg); 113} 114 115var root$b = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"><path fill="currentColor" d="m19.8 22.6l-4.2-4.15q-.875.275-1.762.413T12 19q-3.775 0-6.725-2.087T1 11.5q.525-1.325 1.325-2.463T4.15 7L1.4 4.2l1.4-1.4l18.4 18.4zM12 16q.275 0 .513-.025t.512-.1l-5.4-5.4q-.075.275-.1.513T7.5 11.5q0 1.875 1.313 3.188T12 16m7.3.45l-3.175-3.15q.175-.425.275-.862t.1-.938q0-1.875-1.312-3.187T12 7q-.5 0-.937.1t-.863.3L7.65 4.85q1.025-.425 2.1-.637T12 4q3.775 0 6.725 2.088T23 11.5q-.575 1.475-1.513 2.738T19.3 16.45m-4.625-4.6l-3-3q.7-.125 1.288.113t1.012.687t.613 1.038t.087 1.162"></path></svg>`); 116 117function VisibilityOff($$anchor) { 118 var svg = root$b(); 119 120 append($$anchor, svg); 121} 122 123var root$a = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 256 256"><path fill="currentColor" d="M144 120v88a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8v-88a8 8 0 0 1 8-8h88a8 8 0 0 1 8 8m64 56a8 8 0 0 0-8 8v16h-24a8 8 0 0 0 0 16h24a16 16 0 0 0 16-16v-16a8 8 0 0 0-8-8m0-72a8 8 0 0 0-8 8v32a8 8 0 0 0 16 0v-32a8 8 0 0 0-8-8m-8-64h-16a8 8 0 0 0 0 16h16v16a8 8 0 0 0 16 0V56a16 16 0 0 0-16-16m-56 0h-32a8 8 0 0 0 0 16h32a8 8 0 0 0 0-16M48 88a8 8 0 0 0 8-8V56h16a8 8 0 0 0 0-16H56a16 16 0 0 0-16 16v24a8 8 0 0 0 8 8"></path></svg>`); 124 125function Resize($$anchor) { 126 var svg = root$a(); 127 128 append($$anchor, svg); 129} 130 131var root$9 = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21l-4.343-4.343m0 0A8 8 0 1 0 5.343 5.343a8 8 0 0 0 11.314 11.314M11 8v6m-3-3h6"></path></svg>`); 132 133function ZoomIn($$anchor) { 134 var svg = root$9(); 135 136 append($$anchor, svg); 137} 138 139var root$8 = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21l-4.343-4.343m0 0A8 8 0 1 0 5.343 5.343a8 8 0 0 0 11.314 11.314M8 11h6"></path></svg>`); 140 141function ZoomOut($$anchor) { 142 var svg = root$8(); 143 144 append($$anchor, svg); 145} 146 147class CommandNode { 148 command; 149 next; 150 previous; 151 constructor(command) { 152 this.command = command || null; 153 this.next = null; 154 this.previous = null; 155 } 156 push(command) { 157 this.next = null; 158 const node = new CommandNode(command); 159 node.previous = this; 160 this.next = node; 161 } 162} 163class CommandManager { 164 history = new CommandNode(); 165 current_history = writable(this.history); 166 undo() { 167 if (this.history.previous) { 168 this.history.command?.undo(); 169 this.history = this.history.previous; 170 this.current_history.update(() => this.history); 171 } 172 } 173 redo(context) { 174 if (this.history.next) { 175 this.history = this.history.next; 176 this.history.command?.execute(context); 177 this.current_history.update(() => this.history); 178 } 179 } 180 async execute(command, context) { 181 await command.execute(context); 182 this.history.push(command); 183 this.history = this.history.next; 184 this.current_history.update(() => this.history); 185 } 186 async wait_for_next_frame() { 187 return new Promise((resolve) => { 188 requestAnimationFrame(() => { 189 resolve(); 190 }); 191 }); 192 } 193 async replay(full_history, context) { 194 while (full_history.previous) { 195 full_history = full_history.previous; 196 } 197 while (full_history.next) { 198 await full_history.next.command.execute(context); 199 full_history = full_history.next; 200 } 201 this.history = full_history; 202 this.current_history.update(() => this.history); 203 } 204 contains(command_name) {
205 let current = this.history; 206 while (current) { 207 if (current.command?.name === command_name) return true; 208 current = current.next; 209 } 210 return false; 211 } 212 reset() { 213 this.history = new CommandNode(); 214 this.current_history.update(() => this.history); 215 } 216} 217 218var root_1$5 = from_html(`<span style="margin-left: 4px;" class="svelte-1lrnmgy"> </span>`); 219var root_2$6 = from_html(`<span style="margin-right: 4px;" class="svelte-1lrnmgy"> </span>`); 220var root$7 = from_html(`<button><!> <div><!></div> <!></button>`); 221 222function IconButton($$anchor, $$props) { 223 push($$props, false); 224 225 const _color = mutable_source(); 226 let Icon = prop($$props, 'Icon', 12); 227 let label = prop($$props, 'label', 12, ""); 228 let show_label = prop($$props, 'show_label', 12, false); 229 let pending = prop($$props, 'pending', 12, false); 230 let size = prop($$props, 'size', 12, "small"); 231 let padded = prop($$props, 'padded', 12, true); 232 let highlight = prop($$props, 'highlight', 12, false); 233 let disabled = prop($$props, 'disabled', 12, false); 234 let hasPopup = prop($$props, 'hasPopup', 12, false); 235 let color = prop($$props, 'color', 12, "var(--block-label-text-color)"); 236 let transparent = prop($$props, 'transparent', 12, false); 237 let background = prop($$props, 'background', 12, "var(--background-fill-primary)"); 238 let offset = prop($$props, 'offset', 12, 0); 239 let label_position = prop($$props, 'label_position', 12, "left"); 240 let roundedness = prop($$props, 'roundedness', 12, "quite"); 241 let onclick = prop($$props, 'onclick', 12, undefined); 242 243 legacy_pre_effect(() => (deep_read_state(highlight()), deep_read_state(color())), () => { 244 set(_color, highlight() ? "var(--color-accent)" : color().toString()); 245 }); 246 247 legacy_pre_effect_reset(); 248 249 var $$exports = { 250 get Icon() { 251 return Icon(); 252 }, 253 254 set Icon($$value) { 255 Icon($$value); 256 flushSync(); 257 }, 258 259 get label() { 260 return label(); 261 }, 262 263 set label($$value) { 264 label($$value); 265 flushSync(); 266 }, 267 268 get show_label() { 269 return show_label(); 270 }, 271 272 set show_label($$value) { 273 show_label($$value); 274 flushSync(); 275 }, 276 277 get pending() { 278 return pending(); 279 }, 280 281 set pending($$value) { 282 pending($$value); 283 flushSync(); 284 }, 285 286 get size() { 287 return size(); 288 }, 289 290 set size($$value) { 291 size($$value); 292 flushSync(); 293 }, 294 295 get padded() { 296 return padded(); 297 }, 298 299 set padded($$value) { 300 padded($$value); 301 flushSync(); 302 }, 303 304 get highlight() { 305 return highlight(); 306 }, 307 308 set highlight($$value) { 309 highlight($$value); 310 flushSync(); 311 }, 312 313 get disabled() { 314 return disabled(); 315 }, 316 317 set disabled($$value) { 318 disabled($$value); 319 flushSync(); 320 }, 321 322 get hasPopup() { 323 return hasPopup(); 324 }, 325 326 set hasPopup($$value) { 327 hasPopup($$value); 328 flushSync(); 329 }, 330 331 get color() { 332 return color(); 333 }, 334 335 set color($$value) { 336 color($$value); 337 flushSync(); 338 }, 339 340 get transparent() { 341 return transparent(); 342 }, 343 344 set transparent($$value) { 345 transparent($$value); 346 flushSync(); 347 }, 348 349 get background() { 350 return background(); 351 }, 352 353 set background($$value) { 354 background($$value); 355 flushSync(); 356 }, 357 358 get offset() { 359 return offset(); 360 }, 361 362 set offset($$value) { 363 offset($$value); 364 flushSync(); 365 }, 366 367 get label_position() { 368 return label_position(); 369 }, 370 371 set label_position($$value) { 372 label_position($$value); 373 flushSync(); 374 }, 375 376 get roundedness() { 377 return roundedness(); 378 }, 379 380 set roundedness($$value) { 381 roundedness($$value); 382 flushSync(); 383 }, 384 385 get onclick() { 386 return onclick(); 387 }, 388 389 set onclick($$value) { 390 onclick($$value); 391 flushSync(); 392 } 393 }; 394 395 init$1(); 396 397 var button = root$7(); 398 399 button.__click = function (...$$args) { 400 onclick()?.apply(this, $$args); 401 }; 402 403 let classes; 404 let styles; 405 var node = child(button); 406 407 { 408 var consequent = ($$anchor) => { 409 var span = root_1$5(); 410 var text = child(span, true); 411 412 reset(span); 413 template_effect(() => set_text(text, label())); 414 append($$anchor, span); 415 }; 416 417 if_block(node, ($$render) => { 418 if (show_label() && label_position() === "left") $$render(consequent); 419 }); 420 } 421 422 var div = sibling(node, 2); 423 let classes_1;
424 var node_1 = child(div); 425 426 Icon()(node_1, {}); 427 reset(div); 428 429 var node_2 = sibling(div, 2); 430 431 { 432 var consequent_1 = ($$anchor) => { 433 var span_1 = root_2$6(); 434 var text_1 = child(span_1, true); 435 436 reset(span_1); 437 template_effect(() => set_text(text_1, label())); 438 append($$anchor, span_1); 439 }; 440 441 if_block(node_2, ($$render) => { 442 if (show_label() && label_position() === "right") $$render(consequent_1); 443 }); 444 } 445 446 reset(button); 447 448 template_effect(() => { 449 button.disabled = disabled(); 450 set_attribute(button, 'aria-label', label()); 451 set_attribute(button, 'aria-haspopup', hasPopup()); 452 set_attribute(button, 'title', label()); 453 454 classes = set_class(button, 1, `${roundedness() ?? ''}-round`, 'svelte-1lrnmgy', classes, { 455 pending: pending(), 456 padded: padded(), 457 highlight: highlight(), 458 transparent: transparent() 459 }); 460 461 styles = set_style(button, '', styles, { 462 color: !disabled() && get(_color) ? get(_color) : "var(--block-label-text-color)", 463 '--bg-color': !disabled() ? background() : "auto", 464 'margin-left': offset() + "px" 465 }); 466 467 classes_1 = set_class(div, 1, 'svelte-1lrnmgy', null, classes_1, { 468 small: size() === "small", 469 large: size() === "large", 470 medium: size() === "medium" 471 }); 472 }); 473 474 append($$anchor, button); 475 476 return pop($$exports); 477} 478 479delegate(['click']); 480 481function click_outside(node, callback) { 482 const handle_click = (event) => { 483 if (node && !node.contains(event.target) && !event.defaultPrevented) { 484 callback(event); 485 } 486 }; 487 document.addEventListener("mousedown", handle_click, true); 488 return { 489 destroy() { 490 document.removeEventListener("mousedown", handle_click, true); 491 } 492 }; 493} 494 495var ExtensionType = /* @__PURE__ */ ((ExtensionType2) => { 496 ExtensionType2["Application"] = "application"; 497 ExtensionType2["WebGLPipes"] = "webgl-pipes"; 498 ExtensionType2["WebGLPipesAdaptor"] = "webgl-pipes-adaptor"; 499 ExtensionType2["WebGLSystem"] = "webgl-system"; 500 ExtensionType2["WebGPUPipes"] = "webgpu-pipes"; 501 ExtensionType2["WebGPUPipesAdaptor"] = "webgpu-pipes-adaptor"; 502 ExtensionType2["WebGPUSystem"] = "webgpu-system"; 503 ExtensionType2["CanvasSystem"] = "canvas-system"; 504 ExtensionType2["CanvasPipesAdaptor"] = "canvas-pipes-adaptor"; 505 ExtensionType2["CanvasPipes"] = "canvas-pipes"; 506 ExtensionType2["Asset"] = "asset"; 507 ExtensionType2["LoadParser"] = "load-parser"; 508 ExtensionType2["ResolveParser"] = "resolve-parser"; 509 ExtensionType2["CacheParser"] = "cache-parser"; 510 ExtensionType2["DetectionParser"] = "detection-parser"; 511 ExtensionType2["MaskEffect"] = "mask-effect"; 512 ExtensionType2["BlendMode"] = "blend-mode"; 513 ExtensionType2["TextureSource"] = "texture-source"; 514 ExtensionType2["Environment"] = "environment"; 515 ExtensionType2["ShapeBuilder"] = "shape-builder"; 516 ExtensionType2["Batcher"] = "batcher"; 517 return ExtensionType2; 518})(ExtensionType || {}); 519const normalizeExtension = (ext) => { 520 if (typeof ext === "function" || typeof ext === "object" && ext.extension) { 521 if (!ext.extension) { 522 throw new Error("Extension class must have an extension object"); 523 } 524 const metadata = typeof ext.extension !== "object" ? { type: ext.extension } : ext.extension; 525 ext = { ...metadata, ref: ext }; 526 } 527 if (typeof ext === "object") { 528 ext = { ...ext }; 529 } else { 530 throw new Error("Invalid extension type"); 531 } 532 if (typeof ext.type === "string") { 533 ext.type = [ext.type]; 534 } 535 return ext; 536}; 537const normalizeExtensionPriority = (ext, defaultPriority) => normalizeExtension(ext).priority ?? defaultPriority; 538const extensions = { 539 /** @ignore */ 540 _addHandlers: {}, 541 /** @ignore */ 542 _removeHandlers: {}, 543 /** @ignore */ 544 _queue: {}, 545 /** 546 * Remove extensions from PixiJS. 547 * @param extensions - Extensions to be removed. Can be: 548 * - Extension class with static `extension` property 549 * - Extension format object with `type` and `ref` 550 * - Multiple extensions as separate arguments 551 * @returns {extensions} this for chaining 552 * @example 553 * ```ts 554 * // Remove a single extension 555 * extensions.remove(MyRendererPlugin); 556 * 557 * // Remove multiple extensions 558 * extensions.remove( 559 * MyRendererPlugin, 560 * MySystemPlugin 561 * ); 562 * ``` 563 * @see {@link ExtensionType} For available extension types 564 * @see {@link ExtensionFormat} For extension format details 565 */ 566 remove(...extensions2) {
567 extensions2.map(normalizeExtension).forEach((ext) => { 568 ext.type.forEach((type) => this._removeHandlers[type]?.(ext)); 569 }); 570 return this; 571 }, 572 /** 573 * Register new extensions with PixiJS. Extensions can be registered in multiple formats: 574 * - As a class with a static `extension` property 575 * - As an extension format object 576 * - As multiple extensions passed as separate arguments 577 * @param extensions - Extensions to add to PixiJS. Each can be: 578 * - A class with static `extension` property 579 * - An extension format object with `type` and `ref` 580 * - Multiple extensions as separate arguments 581 * @returns This extensions instance for chaining 582 * @example 583 * ```ts 584 * // Register a simple extension 585 * extensions.add(MyRendererPlugin); 586 * 587 * // Register multiple extensions 588 * extensions.add( 589 * MyRendererPlugin, 590 * MySystemPlugin, 591 * }); 592 * ``` 593 * @see {@link ExtensionType} For available extension types 594 * @see {@link ExtensionFormat} For extension format details 595 * @see {@link extensions.remove} For removing registered extensions 596 */ 597 add(...extensions2) { 598 extensions2.map(normalizeExtension).forEach((ext) => { 599 ext.type.forEach((type) => { 600 const handlers = this._addHandlers; 601 const queue = this._queue; 602 if (!handlers[type]) { 603 queue[type] = queue[type] || []; 604 queue[type]?.push(ext); 605 } else { 606 handlers[type]?.(ext); 607 } 608 }); 609 }); 610 return this; 611 }, 612 /** 613 * Internal method to handle extensions by name. 614 * @param type - The extension type. 615 * @param onAdd - Function handler when extensions are added/registered {@link StrictExtensionFormat}. 616 * @param onRemove - Function handler when extensions are removed/unregistered {@link StrictExtensionFormat}. 617 * @returns this for chaining. 618 * @internal 619 * @ignore 620 */ 621 handle(type, onAdd, onRemove) { 622 const addHandlers = this._addHandlers; 623 const removeHandlers = this._removeHandlers; 624 if (addHandlers[type] || removeHandlers[type]) { 625 throw new Error(`Extension type ${type} already has a handler`); 626 } 627 addHandlers[type] = onAdd; 628 removeHandlers[type] = onRemove; 629 const queue = this._queue; 630 if (queue[type]) { 631 queue[type]?.forEach((ext) => onAdd(ext)); 632 delete queue[type]; 633 } 634 return this; 635 }, 636 /** 637 * Handle a type, but using a map by `name` property. 638 * @param type - Type of extension to handle. 639 * @param map - The object map of named extensions. 640 * @returns this for chaining. 641 * @ignore 642 */ 643 handleByMap(type, map) { 644 return this.handle( 645 type, 646 (extension) => { 647 if (extension.name) { 648 map[extension.name] = extension.ref; 649 } 650 }, 651 (extension) => { 652 if (extension.name) { 653 delete map[extension.name]; 654 } 655 } 656 ); 657 }, 658 /** 659 * Handle a type, but using a list of extensions with a `name` property. 660 * @param type - Type of extension to handle. 661 * @param map - The array of named extensions. 662 * @param defaultPriority - Fallback priority if none is defined. 663 * @returns this for chaining. 664 * @ignore 665 */ 666 handleByNamedList(type, map, defaultPriority = -1) { 667 return this.handle( 668 type, 669 (extension) => { 670 const index = map.findIndex((item) => item.name === extension.name); 671 if (index >= 0) 672 return; 673 map.push({ name: extension.name, value: extension.ref }); 674 map.sort((a, b) => normalizeExtensionPriority(b.value, defaultPriority) - normalizeExtensionPriority(a.value, defaultPriority)); 675 }, 676 (extension) => { 677 const index = map.findIndex((item) => item.name === extension.name); 678 if (index !== -1) { 679 map.splice(index, 1); 680 } 681 } 682 ); 683 }, 684 /** 685 * Handle a type, but using a list of extensions. 686 * @param type - Type of extension to handle. 687 * @param list - The list of extensions. 688 * @param defaultPriority - The default priority to use if none is specified. 689 * @returns this for chaining. 690 * @ignore 691 */ 692 handleByList(type, list, defaultPriority = -1) { 693 return this.handle( 694 type, 695 (extension) => { 696 if (list.includes(extension.ref)) { 697 return; 698 } 699 list.push(extension.ref); 700 list.sort((a, b) => normalizeExtensionPriority(b, defaultPriority) - normalizeExtensionPriority(a, defaultPriority)); 701 }, 702 (extension) => { 703 const index = list.indexOf(extension.ref); 704 if (index !== -1) { 705 list.splice(index, 1); 706 } 707 } 708 ); 709 }, 710 /** 711 * Mixin the source object(s) properties into the target class's prototype. 712 * Copies all property descriptors from source objects to the target's prototype. 713 * @param Target - The target class to mix properties into 714 * @param sources - One or more source objects containing properties to mix in 715 * @example 716 * ```ts 717 * // Create a mixin with shared properties 718 * const moveable = { 719 * x: 0, 720 * y: 0, 721 * move(x: number, y: number) { 722 * this.x += x; 723 * this.y += y; 724 * } 725 * }; 726 * 727 * // Create a mixin with computed properties 728 * const scalable = { 729 * scale: 1, 730 * get scaled() { 731 * return this.scale > 1; 732 * } 733 * }; 734 * 735 * // Apply mixins to a class 736 * extensions.mixin(Sprite, moveable, scalable); 737 * 738 * // Use mixed-in properties 739 * const sprite = new Sprite(); 740 * sprite.move(10, 20); 741 * console.log(sprite.x, sprite.y); // 10, 20 742 * ``` 743 * @remarks 744 * - Copies all properties including getters/setters 745 * - Does not modify source objects 746 * - Preserves property descriptors 747 * @see {@link Object.defineProperties} For details on property descriptors 748 * @see {@link Object.getOwnPropertyDescriptors} For details on property copying 749 */ 750 mixin(Target, ...sources) { 751 for (const source of sources) { 752 Object.defineProperties(Target.prototype, Object.getOwnPropertyDescriptors(source)); 753 } 754 } 755}; 756 757const browserExt = { 758 extension: { 759 type: ExtensionType.Environment, 760 name: "browser", 761 priority: -1 762 }, 763 test: () => true, 764 load: async () => { 765 await __vitePreload(() => import('./browserAll-DiLbNwOz.js'),true ?__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54]):void 0,import.meta.url); 766 } 767}; 768 769const webworkerExt = { 770 extension: { 771 type: ExtensionType.Environment,
772 name: "webworker", 773 priority: 0 774 }, 775 test: () => typeof self !== "undefined" && self.WorkerGlobalScope !== void 0, 776 load: async () => { 777 await __vitePreload(() => import('./webworkerAll-acwYx1_h.js'),true ?__vite__mapDeps([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54]):void 0,import.meta.url); 778 } 779}; 780 781class ObservablePoint { 782 /** 783 * Creates a new `ObservablePoint` 784 * @param observer - Observer to pass to listen for change events. 785 * @param {number} [x=0] - position of the point on the x axis 786 * @param {number} [y=0] - position of the point on the y axis 787 */ 788 constructor(observer, x, y) { 789 this._x = x || 0; 790 this._y = y || 0; 791 this._observer = observer; 792 } 793 /** 794 * Creates a clone of this point. 795 * @example 796 * ```ts 797 * // Basic cloning 798 * const point = new ObservablePoint(observer, 100, 200); 799 * const copy = point.clone(); 800 * 801 * // Clone with new observer 802 * const newObserver = { 803 * _onUpdate: (p) => console.log(`Clone updated: (${p.x}, ${p.y})`) 804 * }; 805 * const watched = point.clone(newObserver); 806 * 807 * // Verify independence 808 * watched.set(300, 400); // Only triggers new observer 809 * ``` 810 * @param observer - Optional observer to pass to the new observable point 811 * @returns A copy of this observable point 812 * @see {@link ObservablePoint.copyFrom} For copying into existing point 813 * @see {@link Observer} For observer interface details 814 */ 815 clone(observer) { 816 return new ObservablePoint(observer ?? this._observer, this._x, this._y); 817 } 818 /** 819 * Sets the point to a new x and y position. 820 * 821 * If y is omitted, both x and y will be set to x. 822 * @example 823 * ```ts 824 * // Basic position setting 825 * const point = new ObservablePoint(observer); 826 * point.set(100, 200); 827 * 828 * // Set both x and y to same value 829 * point.set(50); // x=50, y=50 830 * ``` 831 * @param x - Position on the x axis 832 * @param y - Position on the y axis, defaults to x 833 * @returns The point instance itself 834 * @see {@link ObservablePoint.copyFrom} For copying from another point 835 * @see {@link ObservablePoint.equals} For comparing positions 836 */ 837 set(x = 0, y = x) { 838 if (this._x !== x || this._y !== y) { 839 this._x = x; 840 this._y = y; 841 this._observer._onUpdate(this); 842 } 843 return this; 844 } 845 /** 846 * Copies x and y from the given point into this point. 847 * @example 848 * ```ts 849 * // Basic copying 850 * const source = new ObservablePoint(observer, 100, 200); 851 * const target = new ObservablePoint(); 852 * target.copyFrom(source); 853 * 854 * // Copy and chain operations 855 * const point = new ObservablePoint() 856 * .copyFrom(source) 857 * .set(x + 50, y + 50); 858 * 859 * // Copy from any PointData 860 * const data = { x: 10, y: 20 }; 861 * point.copyFrom(data); 862 * ``` 863 * @param p - The point to copy from 864 * @returns The point instance itself 865 * @see {@link ObservablePoint.copyTo} For copying to another point 866 * @see {@link ObservablePoint.clone} For creating new point copy 867 */ 868 copyFrom(p) { 869 if (this._x !== p.x || this._y !== p.y) { 870 this._x = p.x; 871 this._y = p.y; 872 this._observer._onUpdate(this); 873 } 874 return this; 875 } 876 /** 877 * Copies this point's x and y into the given point. 878 * @example 879 * ```ts 880 * // Basic copying 881 * const source = new ObservablePoint(100, 200); 882 * const target = new ObservablePoint(); 883 * source.copyTo(target); 884 * ``` 885 * @param p - The point to copy to. Can be any type that is or extends `PointLike` 886 * @returns The point (`p`) with values updated 887 * @see {@link ObservablePoint.copyFrom} For copying from another point 888 * @see {@link ObservablePoint.clone} For creating new point copy 889 */ 890 copyTo(p) { 891 p.set(this._x, this._y); 892 return p; 893 } 894 /** 895 * Checks if another point is equal to this point. 896 * 897 * Compares x and y values using strict equality. 898 * @example 899 * ```ts 900 * // Basic equality check 901 * const p1 = new ObservablePoint(100, 200); 902 * const p2 = new ObservablePoint(100, 200); 903 * console.log(p1.equals(p2)); // true 904 * 905 * // Compare with PointData 906 * const data = { x: 100, y: 200 }; 907 * console.log(p1.equals(data)); // true 908 * 909 * // Check different points 910 * const p3 = new ObservablePoint(200, 300); 911 * console.log(p1.equals(p3)); // false 912 * ``` 913 * @param p - The point to check 914 * @returns `true` if both `x` and `y` are equal 915 * @see {@link ObservablePoint.copyFrom} For making points equal 916 * @see {@link PointData} For point data interface 917 */ 918 equals(p) { 919 return p.x === this._x && p.y === this._y; 920 } 921 toString() { 922 return `[pixi.js/math:ObservablePoint x=${this._x} y=${this._y} scope=${this._observer}]`; 923 } 924 /** 925 * Position of the observable point on the x axis. 926 * Triggers observer callback when value changes. 927 * @example 928 * ```ts 929 * // Basic x position 930 * const point = new ObservablePoint(observer); 931 * point.x = 100; // Triggers observer 932 * 933 * // Use in calculations 934 * const width = rightPoint.x - leftPoint.x; 935 * ``` 936 * @default 0 937 */ 938 get x() { 939 return this._x; 940 } 941 set x(value) { 942 if (this._x !== value) { 943 this._x = value; 944 this._observer._onUpdate(this); 945 } 946 } 947 /** 948 * Position of the observable point on the y axis. 949 * Triggers observer callback when value changes. 950 * @example 951 * ```ts 952 * // Basic y position 953 * const point = new ObservablePoint(observer); 954 * point.y = 200; // Triggers observer 955 * 956 * // Use in calculations 957 * const height = bottomPoint.y - topPoint.y; 958 * ``` 959 * @default 0 960 */ 961 get y() { 962 return this._y; 963 } 964 set y(value) { 965 if (this._y !== value) { 966 this._y = value; 967 this._observer._onUpdate(this); 968 }
vendor: 9,997 bytes, lines 968-1319
968 969 } 970} 971 972var eventemitter3 = {exports: {}}; 973 974var hasRequiredEventemitter3; 975 976function requireEventemitter3 () { 977 if (hasRequiredEventemitter3) return eventemitter3.exports; 978 hasRequiredEventemitter3 = 1; 979 (function (module) { 980 981 var has = Object.prototype.hasOwnProperty 982 , prefix = '~'; 983 984 /** 985 * Constructor to create a storage for our `EE` objects. 986 * An `Events` instance is a plain object whose properties are event names. 987 * 988 * @constructor 989 * @private 990 */ 991 function Events() {} 992 993 // 994 // We try to not inherit from `Object.prototype`. In some engines creating an 995 // instance in this way is faster than calling `Object.create(null)` directly. 996 // If `Object.create(null)` is not supported we prefix the event names with a 997 // character to make sure that the built-in object properties are not 998 // overridden or used as an attack vector. 999 // 1000 if (Object.create) { 1001 Events.prototype = Object.create(null); 1002 1003 // 1004 // This hack is needed because the `__proto__` property is still inherited in 1005 // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. 1006 // 1007 if (!new Events().__proto__) prefix = false; 1008 } 1009 1010 /** 1011 * Representation of a single event listener. 1012 * 1013 * @param {Function} fn The listener function. 1014 * @param {*} context The context to invoke the listener with. 1015 * @param {Boolean} [once=false] Specify if the listener is a one-time listener. 1016 * @constructor 1017 * @private 1018 */ 1019 function EE(fn, context, once) { 1020 this.fn = fn; 1021 this.context = context; 1022 this.once = once || false; 1023 } 1024 1025 /** 1026 * Add a listener for a given event. 1027 * 1028 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. 1029 * @param {(String|Symbol)} event The event name. 1030 * @param {Function} fn The listener function. 1031 * @param {*} context The context to invoke the listener with. 1032 * @param {Boolean} once Specify if the listener is a one-time listener. 1033 * @returns {EventEmitter} 1034 * @private 1035 */ 1036 function addListener(emitter, event, fn, context, once) { 1037 if (typeof fn !== 'function') { 1038 throw new TypeError('The listener must be a function'); 1039 } 1040 1041 var listener = new EE(fn, context || emitter, once) 1042 , evt = prefix ? prefix + event : event; 1043 1044 if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; 1045 else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); 1046 else emitter._events[evt] = [emitter._events[evt], listener]; 1047 1048 return emitter; 1049 } 1050 1051 /** 1052 * Clear event by name. 1053 * 1054 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. 1055 * @param {(String|Symbol)} evt The Event name. 1056 * @private 1057 */ 1058 function clearEvent(emitter, evt) { 1059 if (--emitter._eventsCount === 0) emitter._events = new Events(); 1060 else delete emitter._events[evt]; 1061 } 1062 1063 /** 1064 * Minimal `EventEmitter` interface that is molded against the Node.js 1065 * `EventEmitter` interface. 1066 * 1067 * @constructor 1068 * @public 1069 */ 1070 function EventEmitter() { 1071 this._events = new Events(); 1072 this._eventsCount = 0; 1073 } 1074 1075 /** 1076 * Return an array listing the events for which the emitter has registered 1077 * listeners. 1078 * 1079 * @returns {Array} 1080 * @public 1081 */ 1082 EventEmitter.prototype.eventNames = function eventNames() { 1083 var names = [] 1084 , events 1085 , name; 1086 1087 if (this._eventsCount === 0) return names; 1088 1089 for (name in (events = this._events)) { 1090 if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); 1091 } 1092 1093 if (Object.getOwnPropertySymbols) { 1094 return names.concat(Object.getOwnPropertySymbols(events)); 1095 } 1096 1097 return names; 1098 }; 1099 1100 /** 1101 * Return the listeners registered for a given event. 1102 * 1103 * @param {(String|Symbol)} event The event name. 1104 * @returns {Array} The registered listeners. 1105 * @public 1106 */ 1107 EventEmitter.prototype.listeners = function listeners(event) { 1108 var evt = prefix ? prefix + event : event 1109 , handlers = this._events[evt]; 1110 1111 if (!handlers) return []; 1112 if (handlers.fn) return [handlers.fn]; 1113 1114 for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { 1115 ee[i] = handlers[i].fn; 1116 } 1117 1118 return ee; 1119 }; 1120 1121 /** 1122 * Return the number of listeners listening to a given event. 1123 * 1124 * @param {(String|Symbol)} event The event name. 1125 * @returns {Number} The number of listeners. 1126 * @public 1127 */ 1128 EventEmitter.prototype.listenerCount = function listenerCount(event) { 1129 var evt = prefix ? prefix + event : event 1130 , listeners = this._events[evt]; 1131 1132 if (!listeners) return 0; 1133 if (listeners.fn) return 1; 1134 return listeners.length; 1135 }; 1136 1137 /** 1138 * Calls each of the listeners registered for a given event. 1139 * 1140 * @param {(String|Symbol)} event The event name. 1141 * @returns {Boolean} `true` if the event had listeners, else `false`. 1142 * @public 1143 */ 1144 EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { 1145 var evt = prefix ? prefix + event : event; 1146 1147 if (!this._events[evt]) return false; 1148 1149 var listeners = this._events[evt] 1150 , len = arguments.length 1151 , args 1152 , i; 1153 1154 if (listeners.fn) { 1155 if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); 1156 1157 switch (len) { 1158 case 1: return listeners.fn.call(listeners.context), true; 1159 case 2: return listeners.fn.call(listeners.context, a1), true; 1160 case 3: return listeners.fn.call(listeners.context, a1, a2), true; 1161 case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; 1162 case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; 1163 case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; 1164 } 1165 1166 for (i = 1, args = new Array(len -1); i < len; i++) { 1167 args[i - 1] = arguments[i]; 1168 } 1169 1170 listeners.fn.apply(listeners.context, args); 1171 } else { 1172 var length = listeners.length 1173 , j; 1174 1175 for (i = 0; i < length; i++) { 1176 if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); 1177 1178 switch (len) { 1179 case 1: listeners[i].fn.call(listeners[i].context); break; 1180 case 2: listeners[i].fn.call(listeners[i].context, a1); break; 1181 case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; 1182 case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; 1183 default: 1184 if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { 1185 args[j - 1] = arguments[j]; 1186 } 1187 1188 listeners[i].fn.apply(listeners[i].context, args); 1189 } 1190 } 1191 } 1192 1193 return true; 1194 }; 1195 1196 /** 1197 * Add a listener for a given event. 1198 * 1199 * @param {(String|Symbol)} event The event name. 1200 * @param {Function} fn The listener function. 1201 * @param {*} [context=this] The context to invoke the listener with. 1202 * @returns {EventEmitter} `this`. 1203 * @public 1204 */ 1205 EventEmitter.prototype.on = function on(event, fn, context) { 1206 return addListener(this, event, fn, context, false); 1207 }; 1208 1209 /** 1210 * Add a one-time listener for a given event. 1211 * 1212 * @param {(String|Symbol)} event The event name. 1213 * @param {Function} fn The listener function. 1214 * @param {*} [context=this] The context to invoke the listener with. 1215 * @returns {EventEmitter} `this`. 1216 * @public 1217 */ 1218 EventEmitter.prototype.once = function once(event, fn, context) { 1219 return addListener(this, event, fn, context, true); 1220 }; 1221 1222 /** 1223 * Remove the listeners of a given event. 1224 * 1225 * @param {(String|Symbol)} event The event name. 1226 * @param {Function} fn Only remove the listeners that match this function. 1227 * @param {*} context Only remove the listeners that have this context. 1228 * @param {Boolean} once Only remove one-time listeners. 1229 * @returns {EventEmitter} `this`. 1230 * @public 1231 */ 1232 EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { 1233 var evt = prefix ? prefix + event : event; 1234 1235 if (!this._events[evt]) return this; 1236 if (!fn) { 1237 clearEvent(this, evt); 1238 return this; 1239 } 1240 1241 var listeners = this._events[evt]; 1242 1243 if (listeners.fn) { 1244 if ( 1245 listeners.fn === fn && 1246 (!once || listeners.once) && 1247 (!context || listeners.context === context) 1248 ) { 1249 clearEvent(this, evt); 1250 } 1251 } else { 1252 for (var i = 0, events = [], length = listeners.length; i < length; i++) { 1253 if ( 1254 listeners[i].fn !== fn || 1255 (once && !listeners[i].once) || 1256 (context && listeners[i].context !== context) 1257 ) { 1258 events.push(listeners[i]); 1259 } 1260 } 1261 1262 // 1263 // Reset the array, or remove it completely if we have no more listeners. 1264 // 1265 if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; 1266 else clearEvent(this, evt); 1267 } 1268 1269 return this; 1270 }; 1271 1272 /** 1273 * Remove all listeners, or those of the specified event. 1274 * 1275 * @param {(String|Symbol)} [event] The event name. 1276 * @returns {EventEmitter} `this`. 1277 * @public 1278 */ 1279 EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { 1280 var evt; 1281 1282 if (event) { 1283 evt = prefix ? prefix + event : event; 1284 if (this._events[evt]) clearEvent(this, evt); 1285 } else { 1286 this._events = new Events(); 1287 this._eventsCount = 0; 1288 } 1289 1290 return this; 1291 }; 1292 1293 // 1294 // Alias methods names because people roll like that. 1295 // 1296 EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 1297 EventEmitter.prototype.addListener = EventEmitter.prototype.on; 1298 1299 // 1300 // Expose the prefix. 1301 // 1302 EventEmitter.prefixed = prefix; 1303 1304 // 1305 // Allow `EventEmitter` to be imported as module namespace. 1306 // 1307 EventEmitter.EventEmitter = EventEmitter; 1308 1309 // 1310 // Expose the module. 1311 // 1312 { 1313 module.exports = EventEmitter; 1314 } 1315 } (eventemitter3)); 1316 return eventemitter3.exports; 1317} 1318 1319var eventemitter3Exports = requireEventemitter3();
1320const EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports); 1321 1322const PI_2 = Math.PI * 2; 1323const RAD_TO_DEG = 180 / Math.PI; 1324const DEG_TO_RAD = Math.PI / 180; 1325 1326class Point { 1327 /** 1328 * Creates a new `Point` 1329 * @param {number} [x=0] - position of the point on the x axis 1330 * @param {number} [y=0] - position of the point on the y axis 1331 */ 1332 constructor(x = 0, y = 0) { 1333 /** 1334 * Position of the point on the x axis 1335 * @example 1336 * ```ts 1337 * // Set x position 1338 * const point = new Point(); 1339 * point.x = 100; 1340 * 1341 * // Use in calculations 1342 * const width = rightPoint.x - leftPoint.x; 1343 * ``` 1344 */ 1345 this.x = 0; 1346 /** 1347 * Position of the point on the y axis 1348 * @example 1349 * ```ts 1350 * // Set y position 1351 * const point = new Point(); 1352 * point.y = 200; 1353 * 1354 * // Use in calculations 1355 * const height = bottomPoint.y - topPoint.y; 1356 * ``` 1357 */ 1358 this.y = 0; 1359 this.x = x; 1360 this.y = y; 1361 } 1362 /** 1363 * Creates a clone of this point, which is a new instance with the same `x` and `y` values. 1364 * @example 1365 * ```ts 1366 * // Basic point cloning 1367 * const original = new Point(100, 200); 1368 * const copy = original.clone(); 1369 * 1370 * // Clone and modify 1371 * const modified = original.clone(); 1372 * modified.set(300, 400); 1373 * 1374 * // Verify independence 1375 * console.log(original); // Point(100, 200) 1376 * console.log(modified); // Point(300, 400) 1377 * ``` 1378 * @remarks 1379 * - Creates new Point instance 1380 * - Deep copies x and y values 1381 * - Independent from original 1382 * - Useful for preserving values 1383 * @returns A clone of this point 1384 * @see {@link Point.copyFrom} For copying into existing point 1385 * @see {@link Point.copyTo} For copying to existing point 1386 */ 1387 clone() { 1388 return new Point(this.x, this.y); 1389 } 1390 /** 1391 * Copies x and y from the given point into this point. 1392 * @example 1393 * ```ts 1394 * // Basic copying 1395 * const source = new Point(100, 200); 1396 * const target = new Point(); 1397 * target.copyFrom(source); 1398 * 1399 * // Copy and chain operations 1400 * const point = new Point() 1401 * .copyFrom(source) 1402 * .set(x + 50, y + 50); 1403 * 1404 * // Copy from any PointData 1405 * const data = { x: 10, y: 20 }; 1406 * point.copyFrom(data); 1407 * ``` 1408 * @param p - The point to copy from 1409 * @returns The point instance itself 1410 * @see {@link Point.copyTo} For copying to another point 1411 * @see {@link Point.clone} For creating new point copy 1412 */ 1413 copyFrom(p) { 1414 this.set(p.x, p.y); 1415 return this; 1416 } 1417 /** 1418 * Copies this point's x and y into the given point. 1419 * @example 1420 * ```ts 1421 * // Basic copying 1422 * const source = new Point(100, 200); 1423 * const target = new Point(); 1424 * source.copyTo(target); 1425 * ``` 1426 * @param p - The point to copy to. Can be any type that is or extends `PointLike` 1427 * @returns The point (`p`) with values updated 1428 * @see {@link Point.copyFrom} For copying from another point 1429 * @see {@link Point.clone} For creating new point copy 1430 */ 1431 copyTo(p) { 1432 p.set(this.x, this.y); 1433 return p; 1434 } 1435 /** 1436 * Checks if another point is equal to this point. 1437 * 1438 * Compares x and y values using strict equality. 1439 * @example 1440 * ```ts 1441 * // Basic equality check 1442 * const p1 = new Point(100, 200); 1443 * const p2 = new Point(100, 200); 1444 * console.log(p1.equals(p2)); // true 1445 * 1446 * // Compare with PointData 1447 * const data = { x: 100, y: 200 }; 1448 * console.log(p1.equals(data)); // true 1449 * 1450 * // Check different points 1451 * const p3 = new Point(200, 300); 1452 * console.log(p1.equals(p3)); // false 1453 * ``` 1454 * @param p - The point to check 1455 * @returns `true` if both `x` and `y` are equal 1456 * @see {@link Point.copyFrom} For making points equal 1457 * @see {@link PointData} For point data interface 1458 */ 1459 equals(p) { 1460 return p.x === this.x && p.y === this.y; 1461 } 1462 /** 1463 * Sets the point to a new x and y position. 1464 * 1465 * If y is omitted, both x and y will be set to x. 1466 * @example 1467 * ```ts 1468 * // Basic position setting 1469 * const point = new Point(); 1470 * point.set(100, 200); 1471 *
1472 * // Set both x and y to same value 1473 * point.set(50); // x=50, y=50 1474 * 1475 * // Chain with other operations 1476 * point 1477 * .set(10, 20) 1478 * .copyTo(otherPoint); 1479 * ``` 1480 * @param x - Position on the x axis 1481 * @param y - Position on the y axis, defaults to x 1482 * @returns The point instance itself 1483 * @see {@link Point.copyFrom} For copying from another point 1484 * @see {@link Point.equals} For comparing positions 1485 */ 1486 set(x = 0, y = x) { 1487 this.x = x; 1488 this.y = y; 1489 return this; 1490 } 1491 toString() { 1492 return `[pixi.js/math:Point x=${this.x} y=${this.y}]`; 1493 } 1494 /** 1495 * A static Point object with `x` and `y` values of `0`. 1496 * 1497 * This shared instance is reset to zero values when accessed. 1498 * 1499 * > [!IMPORTANT] This point is shared and temporary. Do not store references to it. 1500 * @example 1501 * ```ts 1502 * // Use for temporary calculations 1503 * const tempPoint = Point.shared; 1504 * tempPoint.set(100, 200); 1505 * matrix.apply(tempPoint); 1506 * 1507 * // Will be reset to (0,0) on next access 1508 * const fresh = Point.shared; // x=0, y=0 1509 * ``` 1510 * @readonly 1511 * @returns A fresh zeroed point for temporary use 1512 * @see {@link Point.constructor} For creating new points 1513 * @see {@link PointData} For basic point interface 1514 */ 1515 static get shared() { 1516 tempPoint.x = 0; 1517 tempPoint.y = 0; 1518 return tempPoint; 1519 } 1520} 1521const tempPoint = new Point();
vendor: 16,584 bytes, lines 1522-2045
1522 1523class Matrix { 1524 /** 1525 * @param a - x scale 1526 * @param b - y skew 1527 * @param c - x skew 1528 * @param d - y scale 1529 * @param tx - x translation 1530 * @param ty - y translation 1531 */ 1532 constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { 1533 /** 1534 * Array representation of the matrix. 1535 * Only populated when `toArray()` is called. 1536 * @default null 1537 * @see {@link Matrix.toArray} For filling this array 1538 */ 1539 this.array = null; 1540 this.a = a; 1541 this.b = b; 1542 this.c = c; 1543 this.d = d; 1544 this.tx = tx; 1545 this.ty = ty; 1546 } 1547 /** 1548 * Creates a Matrix object based on the given array. 1549 * Populates matrix components from a flat array in column-major order. 1550 * 1551 * > [!NOTE] Array mapping order: 1552 * > ``` 1553 * > array[0] = a (x scale) 1554 * > array[1] = b (y skew) 1555 * > array[2] = tx (x translation) 1556 * > array[3] = c (x skew) 1557 * > array[4] = d (y scale) 1558 * > array[5] = ty (y translation) 1559 * > ``` 1560 * @example 1561 * ```ts 1562 * // Create matrix from array 1563 * const matrix = new Matrix(); 1564 * matrix.fromArray([ 1565 * 2, 0, 100, // a, b, tx 1566 * 0, 2, 100 // c, d, ty 1567 * ]); 1568 * 1569 * // Create matrix from typed array 1570 * const float32Array = new Float32Array([ 1571 * 1, 0, 0, // Scale x1, no skew 1572 * 0, 1, 0 // No skew, scale x1 1573 * ]); 1574 * matrix.fromArray(float32Array); 1575 * ``` 1576 * @param array - The array to populate the matrix from 1577 * @see {@link Matrix.toArray} For converting matrix to array 1578 * @see {@link Matrix.set} For setting values directly 1579 */ 1580 fromArray(array) { 1581 this.a = array[0]; 1582 this.b = array[1]; 1583 this.c = array[3]; 1584 this.d = array[4]; 1585 this.tx = array[2]; 1586 this.ty = array[5]; 1587 } 1588 /** 1589 * Sets the matrix properties directly. 1590 * All matrix components can be set in one call. 1591 * @example 1592 * ```ts 1593 * // Set to identity matrix 1594 * matrix.set(1, 0, 0, 1, 0, 0); 1595 * 1596 * // Set to scale matrix 1597 * matrix.set(2, 0, 0, 2, 0, 0); // Scale 2x 1598 * 1599 * // Set to translation matrix 1600 * matrix.set(1, 0, 0, 1, 100, 50); // Move 100,50 1601 * ``` 1602 * @param a - Scale on x axis 1603 * @param b - Shear on y axis 1604 * @param c - Shear on x axis 1605 * @param d - Scale on y axis 1606 * @param tx - Translation on x axis 1607 * @param ty - Translation on y axis 1608 * @returns This matrix. Good for chaining method calls. 1609 * @see {@link Matrix.identity} For resetting to identity 1610 * @see {@link Matrix.fromArray} For setting from array 1611 */ 1612 set(a, b, c, d, tx, ty) { 1613 this.a = a; 1614 this.b = b; 1615 this.c = c; 1616 this.d = d; 1617 this.tx = tx; 1618 this.ty = ty; 1619 return this; 1620 } 1621 /** 1622 * Creates an array from the current Matrix object. 1623 * 1624 * > [!NOTE] The array format is: 1625 * > ``` 1626 * > Non-transposed: 1627 * > [a, c, tx, 1628 * > b, d, ty, 1629 * > 0, 0, 1] 1630 * > 1631 * > Transposed: 1632 * > [a, b, 0, 1633 * > c, d, 0, 1634 * > tx,ty,1] 1635 * > ``` 1636 * @example 1637 * ```ts 1638 * // Basic array conversion 1639 * const matrix = new Matrix(2, 0, 0, 2, 100, 100); 1640 * const array = matrix.toArray(); 1641 * 1642 * // Using existing array 1643 * const float32Array = new Float32Array(9); 1644 * matrix.toArray(false, float32Array); 1645 * 1646 * // Get transposed array 1647 * const transposed = matrix.toArray(true); 1648 * ``` 1649 * @param transpose - Whether to transpose the matrix 1650 * @param out - Optional Float32Array to store the result 1651 * @returns The array containing the matrix values 1652 * @see {@link Matrix.fromArray} For creating matrix from array 1653 * @see {@link Matrix.array} For cached array storage 1654 */ 1655 toArray(transpose, out) { 1656 if (!this.array) { 1657 this.array = new Float32Array(9); 1658 } 1659 const array = out || this.array; 1660 if (transpose) { 1661 array[0] = this.a; 1662 array[1] = this.b; 1663 array[2] = 0; 1664 array[3] = this.c; 1665 array[4] = this.d; 1666 array[5] = 0; 1667 array[6] = this.tx; 1668 array[7] = this.ty; 1669 array[8] = 1; 1670 } else { 1671 array[0] = this.a; 1672 array[1] = this.c; 1673 array[2] = this.tx; 1674 array[3] = this.b; 1675 array[4] = this.d; 1676 array[5] = this.ty; 1677 array[6] = 0; 1678 array[7] = 0; 1679 array[8] = 1; 1680 } 1681 return array; 1682 } 1683 /** 1684 * Get a new position with the current transformation applied. 1685 * 1686 * Can be used to go from a child's coordinate space to the world coordinate space. (e.g. rendering) 1687 * @example 1688 * ```ts 1689 * // Basic point transformation 1690 * const matrix = new Matrix().translate(100, 50).rotate(Math.PI / 4); 1691 * const point = new Point(10, 20); 1692 * const transformed = matrix.apply(point); 1693 * 1694 * // Reuse existing point 1695 * const output = new Point(); 1696 * matrix.apply(point, output); 1697 * ``` 1698 * @param pos - The origin point to transform 1699 * @param newPos - Optional point to store the result 1700 * @returns The transformed point 1701 * @see {@link Matrix.applyInverse} For inverse transformation 1702 * @see {@link Point} For point operations 1703 */ 1704 apply(pos, newPos) { 1705 newPos = newPos || new Point(); 1706 const x = pos.x; 1707 const y = pos.y; 1708 newPos.x = this.a * x + this.c * y + this.tx; 1709 newPos.y = this.b * x + this.d * y + this.ty; 1710 return newPos; 1711 } 1712 /** 1713 * Get a new position with the inverse of the current transformation applied. 1714 * 1715 * Can be used to go from the world coordinate space to a child's coordinate space. (e.g. input) 1716 * @example 1717 * ```ts 1718 * // Basic inverse transformation 1719 * const matrix = new Matrix().translate(100, 50).rotate(Math.PI / 4); 1720 * const worldPoint = new Point(150, 100); 1721 * const localPoint = matrix.applyInverse(worldPoint); 1722 * 1723 * // Reuse existing point 1724 * const output = new Point(); 1725 * matrix.applyInverse(worldPoint, output); 1726 * 1727 * // Convert mouse position to local space 1728 * const mousePoint = new Point(mouseX, mouseY); 1729 * const localMouse = matrix.applyInverse(mousePoint); 1730 * ``` 1731 * @param pos - The origin point to inverse-transform 1732 * @param newPos - Optional point to store the result 1733 * @returns The inverse-transformed point 1734 * @see {@link Matrix.apply} For forward transformation 1735 * @see {@link Matrix.invert} For getting inverse matrix 1736 */ 1737 applyInverse(pos, newPos) { 1738 newPos = newPos || new Point(); 1739 const a = this.a; 1740 const b = this.b; 1741 const c = this.c; 1742 const d = this.d; 1743 const tx = this.tx; 1744 const ty = this.ty; 1745 const id = 1 / (a * d + c * -b); 1746 const x = pos.x; 1747 const y = pos.y; 1748 newPos.x = d * id * x + -c * id * y + (ty * c - tx * d) * id; 1749 newPos.y = a * id * y + -b * id * x + (-ty * a + tx * b) * id; 1750 return newPos; 1751 } 1752 /** 1753 * Translates the matrix on the x and y axes. 1754 * Adds to the position values while preserving scale, rotation and skew. 1755 * @example 1756 * ```ts 1757 * // Basic translation 1758 * const matrix = new Matrix(); 1759 * matrix.translate(100, 50); // Move right 100, down 50 1760 * 1761 * // Chain with other transformations 1762 * matrix 1763 * .scale(2, 2) 1764 * .translate(100, 0) 1765 * .rotate(Math.PI / 4); 1766 * ``` 1767 * @param x - How much to translate on the x axis 1768 * @param y - How much to translate on the y axis 1769 * @returns This matrix. Good for chaining method calls. 1770 * @see {@link Matrix.set} For setting position directly 1771 * @see {@link Matrix.setTransform} For complete transform setup 1772 */ 1773 translate(x, y) { 1774 this.tx += x; 1775 this.ty += y; 1776 return this; 1777 } 1778 /** 1779 * Applies a scale transformation to the matrix. 1780 * Multiplies the scale values with existing matrix components. 1781 * @example 1782 * ```ts 1783 * // Basic scaling 1784 * const matrix = new Matrix(); 1785 * matrix.scale(2, 3); // Scale 2x horizontally, 3x vertically 1786 * 1787 * // Chain with other transformations 1788 * matrix 1789 * .translate(100, 100) 1790 * .scale(2, 2) // Scales after translation 1791 * .rotate(Math.PI / 4); 1792 * ``` 1793 * @param x - The amount to scale horizontally 1794 * @param y - The amount to scale vertically 1795 * @returns This matrix. Good for chaining method calls. 1796 * @see {@link Matrix.setTransform} For setting scale directly 1797 * @see {@link Matrix.append} For combining transformations 1798 */ 1799 scale(x, y) { 1800 this.a *= x; 1801 this.d *= y; 1802 this.c *= x; 1803 this.b *= y; 1804 this.tx *= x; 1805 this.ty *= y; 1806 return this; 1807 } 1808 /** 1809 * Applies a rotation transformation to the matrix. 1810 * 1811 * Rotates around the origin (0,0) by the given angle in radians. 1812 * @example 1813 * ```ts 1814 * // Basic rotation 1815 * const matrix = new Matrix(); 1816 * matrix.rotate(Math.PI / 4); // Rotate 45 degrees 1817 * 1818 * // Chain with other transformations 1819 * matrix 1820 * .translate(100, 100) // Move to rotation center 1821 * .rotate(Math.PI) // Rotate 180 degrees 1822 * .scale(2, 2); // Scale after rotation 1823 * 1824 * // Common angles 1825 * matrix.rotate(Math.PI / 2); // 90 degrees 1826 * matrix.rotate(Math.PI); // 180 degrees 1827 * matrix.rotate(Math.PI * 2); // 360 degrees 1828 * ``` 1829 * @remarks 1830 * - Rotates around origin point (0,0) 1831 * - Affects position if translation was set 1832 * - Uses counter-clockwise rotation 1833 * - Order of operations matters when chaining 1834 * @param angle - The angle in radians 1835 * @returns This matrix. Good for chaining method calls. 1836 * @see {@link Matrix.setTransform} For setting rotation directly 1837 * @see {@link Matrix.append} For combining transformations 1838 */ 1839 rotate(angle) { 1840 const cos = Math.cos(angle); 1841 const sin = Math.sin(angle); 1842 const a1 = this.a; 1843 const c1 = this.c; 1844 const tx1 = this.tx; 1845 this.a = a1 * cos - this.b * sin; 1846 this.b = a1 * sin + this.b * cos; 1847 this.c = c1 * cos - this.d * sin; 1848 this.d = c1 * sin + this.d * cos; 1849 this.tx = tx1 * cos - this.ty * sin; 1850 this.ty = tx1 * sin + this.ty * cos; 1851 return this; 1852 } 1853 /** 1854 * Appends the given Matrix to this Matrix. 1855 * Combines two matrices by multiplying them together: this = this * matrix 1856 * @example 1857 * ```ts 1858 * // Basic matrix combination 1859 * const matrix = new Matrix(); 1860 * const other = new Matrix().translate(100, 0).rotate(Math.PI / 4); 1861 * matrix.append(other); 1862 * ``` 1863 * @remarks 1864 * - Order matters: A.append(B) !== B.append(A) 1865 * - Modifies current matrix 1866 * - Preserves transformation order 1867 * - Commonly used for combining transforms 1868 * @param matrix - The matrix to append 1869 * @returns This matrix. Good for chaining method calls. 1870 * @see {@link Matrix.prepend} For prepending transformations 1871 * @see {@link Matrix.appendFrom} For appending two external matrices 1872 */ 1873 append(matrix) { 1874 const a1 = this.a; 1875 const b1 = this.b; 1876 const c1 = this.c; 1877 const d1 = this.d; 1878 this.a = matrix.a * a1 + matrix.b * c1; 1879 this.b = matrix.a * b1 + matrix.b * d1; 1880 this.c = matrix.c * a1 + matrix.d * c1; 1881 this.d = matrix.c * b1 + matrix.d * d1; 1882 this.tx = matrix.tx * a1 + matrix.ty * c1 + this.tx; 1883 this.ty = matrix.tx * b1 + matrix.ty * d1 + this.ty; 1884 return this; 1885 } 1886 /** 1887 * Appends two matrices and sets the result to this matrix. 1888 * Performs matrix multiplication: this = A * B 1889 * @example 1890 * ```ts 1891 * // Basic matrix multiplication 1892 * const result = new Matrix(); 1893 * const matrixA = new Matrix().scale(2, 2); 1894 * const matrixB = new Matrix().rotate(Math.PI / 4); 1895 * result.appendFrom(matrixA, matrixB); 1896 * ``` 1897 * @remarks 1898 * - Order matters: A * B !== B * A 1899 * - Creates a new transformation from two others 1900 * - More efficient than append() for multiple operations 1901 * - Does not modify input matrices 1902 * @param a - The first matrix to multiply 1903 * @param b - The second matrix to multiply 1904 * @returns This matrix. Good for chaining method calls. 1905 * @see {@link Matrix.append} For single matrix combination 1906 * @see {@link Matrix.prepend} For reverse order multiplication 1907 */ 1908 appendFrom(a, b) { 1909 const a1 = a.a; 1910 const b1 = a.b; 1911 const c1 = a.c; 1912 const d1 = a.d; 1913 const tx = a.tx; 1914 const ty = a.ty; 1915 const a2 = b.a; 1916 const b2 = b.b; 1917 const c2 = b.c; 1918 const d2 = b.d; 1919 this.a = a1 * a2 + b1 * c2; 1920 this.b = a1 * b2 + b1 * d2; 1921 this.c = c1 * a2 + d1 * c2; 1922 this.d = c1 * b2 + d1 * d2; 1923 this.tx = tx * a2 + ty * c2 + b.tx; 1924 this.ty = tx * b2 + ty * d2 + b.ty; 1925 return this; 1926 } 1927 /** 1928 * Sets the matrix based on all the available properties. 1929 * Combines position, scale, rotation, skew and pivot in a single operation. 1930 * @example 1931 * ```ts 1932 * // Basic transform setup 1933 * const matrix = new Matrix(); 1934 * matrix.setTransform( 1935 * 100, 100, // position 1936 * 0, 0, // pivot 1937 * 2, 2, // scale 1938 * Math.PI / 4, // rotation (45 degrees) 1939 * 0, 0 // skew 1940 * ); 1941 * ``` 1942 * @remarks 1943 * - Updates all matrix components at once 1944 * - More efficient than separate transform calls 1945 * - Uses radians for rotation and skew 1946 * - Pivot affects rotation center 1947 * @param x - Position on the x axis 1948 * @param y - Position on the y axis 1949 * @param pivotX - Pivot on the x axis 1950 * @param pivotY - Pivot on the y axis 1951 * @param scaleX - Scale on the x axis 1952 * @param scaleY - Scale on the y axis 1953 * @param rotation - Rotation in radians 1954 * @param skewX - Skew on the x axis 1955 * @param skewY - Skew on the y axis 1956 * @returns This matrix. Good for chaining method calls. 1957 * @see {@link Matrix.decompose} For extracting transform properties 1958 * @see {@link TransformableObject} For transform data structure 1959 */ 1960 setTransform(x, y, pivotX, pivotY, scaleX, scaleY, rotation, skewX, skewY) { 1961 this.a = Math.cos(rotation + skewY) * scaleX; 1962 this.b = Math.sin(rotation + skewY) * scaleX; 1963 this.c = -Math.sin(rotation - skewX) * scaleY; 1964 this.d = Math.cos(rotation - skewX) * scaleY; 1965 this.tx = x - (pivotX * this.a + pivotY * this.c); 1966 this.ty = y - (pivotX * this.b + pivotY * this.d); 1967 return this; 1968 } 1969 /** 1970 * Prepends the given Matrix to this Matrix. 1971 * Combines two matrices by multiplying them together: this = matrix * this 1972 * @example 1973 * ```ts 1974 * // Basic matrix prepend 1975 * const matrix = new Matrix().scale(2, 2); 1976 * const other = new Matrix().translate(100, 0); 1977 * matrix.prepend(other); // Translation happens before scaling 1978 * ``` 1979 * @remarks 1980 * - Order matters: A.prepend(B) !== B.prepend(A) 1981 * - Modifies current matrix 1982 * - Reverses transformation order compared to append() 1983 * @param matrix - The matrix to prepend 1984 * @returns This matrix. Good for chaining method calls. 1985 * @see {@link Matrix.append} For appending transformations 1986 * @see {@link Matrix.appendFrom} For combining external matrices 1987 */ 1988 prepend(matrix) { 1989 const tx1 = this.tx; 1990 if (matrix.a !== 1 || matrix.b !== 0 || matrix.c !== 0 || matrix.d !== 1) { 1991 const a1 = this.a; 1992 const c1 = this.c; 1993 this.a = a1 * matrix.a + this.b * matrix.c; 1994 this.b = a1 * matrix.b + this.b * matrix.d; 1995 this.c = c1 * matrix.a + this.d * matrix.c; 1996 this.d = c1 * matrix.b + this.d * matrix.d; 1997 } 1998 this.tx = tx1 * matrix.a + this.ty * matrix.c + matrix.tx; 1999 this.ty = tx1 * matrix.b + this.ty * matrix.d + matrix.ty; 2000 return this; 2001 } 2002 /** 2003 * Decomposes the matrix into its individual transform components. 2004 * Extracts position, scale, rotation and skew values from the matrix. 2005 * @example 2006 * ```ts 2007 * // Basic decomposition 2008 * const matrix = new Matrix() 2009 * .translate(100, 100) 2010 * .rotate(Math.PI / 4) 2011 * .scale(2, 2); 2012 * 2013 * const transform = { 2014 * position: new Point(), 2015 * scale: new Point(), 2016 * pivot: new Point(), 2017 * skew: new Point(), 2018 * rotation: 0 2019 * }; 2020 * 2021 * matrix.decompose(transform); 2022 * console.log(transform.position); // Point(100, 100) 2023 * console.log(transform.rotation); // ~0.785 (PI/4) 2024 * console.log(transform.scale); // Point(2, 2) 2025 * ``` 2026 * @remarks 2027 * - Handles combined transformations 2028 * - Accounts for pivot points 2029 * - Chooses between rotation/skew based on transform type 2030 * - Uses radians for rotation and skew 2031 * @param transform - The transform object to store the decomposed values 2032 * @returns The transform with the newly applied properties 2033 * @see {@link Matrix.setTransform} For composing from components 2034 * @see {@link TransformableObject} For transform structure 2035 */ 2036 decompose(transform) { 2037 const a = this.a; 2038 const b = this.b; 2039 const c = this.c; 2040 const d = this.d; 2041 const pivot = transform.pivot; 2042 const skewX = -Math.atan2(-c, d); 2043 const skewY = Math.atan2(b, a); 2044 const delta = Math.abs(skewX + skewY); 2045
vendor: 7,573 bytes, lines 2045-2292
2045if (delta < 1e-5 || Math.abs(PI_2 - delta) < 1e-5) { 2046 transform.rotation = skewY; 2047 transform.skew.x = transform.skew.y = 0; 2048 } else { 2049 transform.rotation = 0; 2050 transform.skew.x = skewX; 2051 transform.skew.y = skewY; 2052 } 2053 transform.scale.x = Math.sqrt(a * a + b * b); 2054 transform.scale.y = Math.sqrt(c * c + d * d); 2055 transform.position.x = this.tx + (pivot.x * a + pivot.y * c); 2056 transform.position.y = this.ty + (pivot.x * b + pivot.y * d); 2057 return transform; 2058 } 2059 /** 2060 * Inverts this matrix. 2061 * Creates the matrix that when multiplied with this matrix results in an identity matrix. 2062 * @example 2063 * ```ts 2064 * // Basic matrix inversion 2065 * const matrix = new Matrix() 2066 * .translate(100, 50) 2067 * .scale(2, 2); 2068 * 2069 * matrix.invert(); // Now transforms in opposite direction 2070 * 2071 * // Verify inversion 2072 * const point = new Point(50, 50); 2073 * const transformed = matrix.apply(point); 2074 * const original = matrix.invert().apply(transformed); 2075 * // original ≈ point 2076 * ``` 2077 * @remarks 2078 * - Modifies the current matrix 2079 * - Useful for reversing transformations 2080 * - Cannot invert matrices with zero determinant 2081 * @returns This matrix. Good for chaining method calls. 2082 * @see {@link Matrix.identity} For resetting to identity 2083 * @see {@link Matrix.applyInverse} For inverse transformations 2084 */ 2085 invert() { 2086 const a1 = this.a; 2087 const b1 = this.b; 2088 const c1 = this.c; 2089 const d1 = this.d; 2090 const tx1 = this.tx; 2091 const n = a1 * d1 - b1 * c1; 2092 this.a = d1 / n; 2093 this.b = -b1 / n; 2094 this.c = -c1 / n; 2095 this.d = a1 / n; 2096 this.tx = (c1 * this.ty - d1 * tx1) / n; 2097 this.ty = -(a1 * this.ty - b1 * tx1) / n; 2098 return this; 2099 } 2100 /** 2101 * Checks if this matrix is an identity matrix. 2102 * 2103 * An identity matrix has no transformations applied (default state). 2104 * @example 2105 * ```ts 2106 * // Check if matrix is identity 2107 * const matrix = new Matrix(); 2108 * console.log(matrix.isIdentity()); // true 2109 * 2110 * // Check after transformations 2111 * matrix.translate(100, 0); 2112 * console.log(matrix.isIdentity()); // false 2113 * 2114 * // Reset and verify 2115 * matrix.identity(); 2116 * console.log(matrix.isIdentity()); // true 2117 * ``` 2118 * @remarks 2119 * - Verifies a = 1, d = 1 (no scale) 2120 * - Verifies b = 0, c = 0 (no skew) 2121 * - Verifies tx = 0, ty = 0 (no translation) 2122 * @returns True if matrix has no transformations 2123 * @see {@link Matrix.identity} For resetting to identity 2124 * @see {@link Matrix.IDENTITY} For constant identity matrix 2125 */ 2126 isIdentity() { 2127 return this.a === 1 && this.b === 0 && this.c === 0 && this.d === 1 && this.tx === 0 && this.ty === 0; 2128 } 2129 /** 2130 * Resets this Matrix to an identity (default) matrix. 2131 * Sets all components to their default values: scale=1, no skew, no translation. 2132 * @example 2133 * ```ts 2134 * // Reset transformed matrix 2135 * const matrix = new Matrix() 2136 * .scale(2, 2) 2137 * .rotate(Math.PI / 4); 2138 * matrix.identity(); // Back to default state 2139 * 2140 * // Chain after reset 2141 * matrix 2142 * .identity() 2143 * .translate(100, 100) 2144 * .scale(2, 2); 2145 * 2146 * // Compare with identity constant 2147 * const isDefault = matrix.equals(Matrix.IDENTITY); 2148 * ``` 2149 * @remarks 2150 * - Sets a=1, d=1 (default scale) 2151 * - Sets b=0, c=0 (no skew) 2152 * - Sets tx=0, ty=0 (no translation) 2153 * @returns This matrix. Good for chaining method calls. 2154 * @see {@link Matrix.IDENTITY} For constant identity matrix 2155 * @see {@link Matrix.isIdentity} For checking identity state 2156 */ 2157 identity() { 2158 this.a = 1; 2159 this.b = 0; 2160 this.c = 0; 2161 this.d = 1; 2162 this.tx = 0; 2163 this.ty = 0; 2164 return this; 2165 } 2166 /** 2167 * Creates a new Matrix object with the same values as this one. 2168 * @returns A copy of this matrix. Good for chaining method calls. 2169 */ 2170 clone() { 2171 const matrix = new Matrix(); 2172 matrix.a = this.a; 2173 matrix.b = this.b; 2174 matrix.c = this.c; 2175 matrix.d = this.d; 2176 matrix.tx = this.tx; 2177 matrix.ty = this.ty; 2178 return matrix; 2179 } 2180 /** 2181 * Creates a new Matrix object with the same values as this one. 2182 * @param matrix 2183 * @example 2184 * ```ts 2185 * // Basic matrix cloning 2186 * const matrix = new Matrix() 2187 * .translate(100, 100) 2188 * .rotate(Math.PI / 4); 2189 * const copy = matrix.clone(); 2190 * 2191 * // Clone and modify 2192 * const modified = matrix.clone() 2193 * .scale(2, 2); 2194 * 2195 * // Compare matrices 2196 * console.log(matrix.equals(copy)); // true 2197 * console.log(matrix.equals(modified)); // false 2198 * ``` 2199 * @returns A copy of this matrix. Good for chaining method calls. 2200 * @see {@link Matrix.copyTo} For copying to existing matrix 2201 * @see {@link Matrix.copyFrom} For copying from another matrix 2202 */ 2203 copyTo(matrix) { 2204 matrix.a = this.a; 2205 matrix.b = this.b; 2206 matrix.c = this.c; 2207 matrix.d = this.d; 2208 matrix.tx = this.tx; 2209 matrix.ty = this.ty; 2210 return matrix; 2211 } 2212 /** 2213 * Changes the values of the matrix to be the same as the ones in given matrix. 2214 * @example 2215 * ```ts 2216 * // Basic matrix copying 2217 * const source = new Matrix() 2218 * .translate(100, 100) 2219 * .rotate(Math.PI / 4); 2220 * const target = new Matrix(); 2221 * target.copyFrom(source); 2222 * ``` 2223 * @param matrix - The matrix to copy from 2224 * @returns This matrix. Good for chaining method calls. 2225 * @see {@link Matrix.clone} For creating new matrix copy 2226 * @see {@link Matrix.copyTo} For copying to another matrix 2227 */ 2228 copyFrom(matrix) { 2229 this.a = matrix.a; 2230 this.b = matrix.b; 2231 this.c = matrix.c; 2232 this.d = matrix.d; 2233 this.tx = matrix.tx; 2234 this.ty = matrix.ty; 2235 return this; 2236 } 2237 /** 2238 * Checks if this matrix equals another matrix. 2239 * Compares all components for exact equality. 2240 * @example 2241 * ```ts 2242 * // Basic equality check 2243 * const m1 = new Matrix(); 2244 * const m2 = new Matrix(); 2245 * console.log(m1.equals(m2)); // true 2246 * 2247 * // Compare transformed matrices 2248 * const transform = new Matrix() 2249 * .translate(100, 100) 2250 * const clone = new Matrix() 2251 * .scale(2, 2); 2252 * console.log(transform.equals(clone)); // false 2253 * ``` 2254 * @param matrix - The matrix to compare to 2255 * @returns True if matrices are identical 2256 * @see {@link Matrix.copyFrom} For copying matrix values 2257 * @see {@link Matrix.isIdentity} For identity comparison 2258 */ 2259 equals(matrix) { 2260 return matrix.a === this.a && matrix.b === this.b && matrix.c === this.c && matrix.d === this.d && matrix.tx === this.tx && matrix.ty === this.ty; 2261 } 2262 toString() { 2263 return `[pixi.js:Matrix a=${this.a} b=${this.b} c=${this.c} d=${this.d} tx=${this.tx} ty=${this.ty}]`; 2264 } 2265 /** 2266 * A default (identity) matrix with no transformations applied. 2267 * 2268 * > [!IMPORTANT] This is a shared read-only object. Create a new Matrix if you need to modify it. 2269 * @example 2270 * ```ts 2271 * // Get identity matrix reference 2272 * const identity = Matrix.IDENTITY; 2273 * console.log(identity.isIdentity()); // true 2274 * 2275 * // Compare with identity 2276 * const matrix = new Matrix(); 2277 * console.log(matrix.equals(Matrix.IDENTITY)); // true 2278 * 2279 * // Create new matrix instead of modifying IDENTITY 2280 * const transform = new Matrix() 2281 * .copyFrom(Matrix.IDENTITY) 2282 * .translate(100, 100); 2283 * ``` 2284 * @readonly 2285 * @returns A read-only identity matrix 2286 * @see {@link Matrix.shared} For temporary calculations 2287 * @see {@link Matrix.identity} For resetting matrices 2288 */ 2289 static get IDENTITY() { 2290 return identityMatrix$1.identity(); 2291 } 2292 /**
2293 * A static Matrix that can be used to avoid creating new objects. 2294 * Will always ensure the matrix is reset to identity when requested. 2295 * 2296 * > [!IMPORTANT] This matrix is shared and temporary. Do not store references to it. 2297 * @example 2298 * ```ts 2299 * // Use for temporary calculations 2300 * const tempMatrix = Matrix.shared; 2301 * tempMatrix.translate(100, 100).rotate(Math.PI / 4); 2302 * const point = tempMatrix.apply({ x: 10, y: 20 }); 2303 * 2304 * // Will be reset to identity on next access 2305 * const fresh = Matrix.shared; // Back to identity 2306 * ``` 2307 * @remarks 2308 * - Always returns identity matrix 2309 * - Safe to modify temporarily 2310 * - Not safe to store references 2311 * - Useful for one-off calculations 2312 * @readonly 2313 * @returns A fresh identity matrix for temporary use 2314 * @see {@link Matrix.IDENTITY} For immutable identity matrix 2315 * @see {@link Matrix.identity} For resetting matrices 2316 */ 2317 static get shared() { 2318 return tempMatrix$3.identity(); 2319 } 2320} 2321const tempMatrix$3 = new Matrix(); 2322const identityMatrix$1 = new Matrix(); 2323 2324const ux = [1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1]; 2325const uy = [0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1]; 2326const vx = [0, -1, -1, -1, 0, 1, 1, 1, 0, 1, 1, 1, 0, -1, -1, -1]; 2327const vy = [1, 1, 0, -1, -1, -1, 0, 1, -1, -1, 0, 1, 1, 1, 0, -1]; 2328const rotationCayley = []; 2329const rotationMatrices = []; 2330const signum = Math.sign; 2331function init() { 2332 for (let i = 0; i < 16; i++) { 2333 const row = []; 2334 rotationCayley.push(row); 2335 for (let j = 0; j < 16; j++) { 2336 const _ux = signum(ux[i] * ux[j] + vx[i] * uy[j]); 2337 const _uy = signum(uy[i] * ux[j] + vy[i] * uy[j]); 2338 const _vx = signum(ux[i] * vx[j] + vx[i] * vy[j]); 2339 const _vy = signum(uy[i] * vx[j] + vy[i] * vy[j]); 2340 for (let k = 0; k < 16; k++) { 2341 if (ux[k] === _ux && uy[k] === _uy && vx[k] === _vx && vy[k] === _vy) { 2342 row.push(k); 2343 break; 2344 } 2345 } 2346 } 2347 } 2348 for (let i = 0; i < 16; i++) { 2349 const mat = new Matrix(); 2350 mat.set(ux[i], uy[i], vx[i], vy[i], 0, 0); 2351 rotationMatrices.push(mat); 2352 } 2353} 2354init(); 2355const groupD8 = { 2356 /** 2357 * | Rotation | Direction | 2358 * |----------|-----------| 2359 * | 0° | East | 2360 * @group groupD8 2361 * @type {GD8Symmetry} 2362 */ 2363 E: 0, 2364 /** 2365 * | Rotation | Direction | 2366 * |----------|-----------| 2367 * | 45°↻ | Southeast | 2368 * @group groupD8 2369 * @type {GD8Symmetry} 2370 */ 2371 SE: 1, 2372 /** 2373 * | Rotation | Direction | 2374 * |----------|-----------| 2375 * | 90°↻ | South | 2376 * @group groupD8 2377 * @type {GD8Symmetry} 2378 */ 2379 S: 2, 2380 /** 2381 * | Rotation | Direction | 2382 * |----------|-----------| 2383 * | 135°↻ | Southwest | 2384 * @group groupD8 2385 * @type {GD8Symmetry} 2386 */ 2387 SW: 3, 2388 /** 2389 * | Rotation | Direction | 2390 * |----------|-----------| 2391 * | 180° | West | 2392 * @group groupD8 2393 * @type {GD8Symmetry} 2394 */ 2395 W: 4, 2396 /** 2397 * | Rotation | Direction | 2398 * |-------------|--------------| 2399 * | -135°/225°↻ | Northwest | 2400 * @group groupD8 2401 * @type {GD8Symmetry} 2402 */ 2403 NW: 5, 2404 /** 2405 * | Rotation | Direction | 2406 * |-------------|--------------| 2407 * | -90°/270°↻ | North | 2408 * @group groupD8 2409 * @type {GD8Symmetry} 2410 */ 2411 N: 6, 2412 /** 2413 * | Rotation | Direction | 2414 * |-------------|--------------| 2415 * | -45°/315°↻ | Northeast | 2416 * @group groupD8 2417 * @type {GD8Symmetry} 2418 */ 2419 NE: 7, 2420 /** 2421 * Reflection about Y-axis. 2422 * @group groupD8 2423 * @type {GD8Symmetry} 2424 */ 2425 MIRROR_VERTICAL: 8, 2426 /** 2427 * Reflection about the main diagonal. 2428 * @group groupD8 2429 * @type {GD8Symmetry} 2430 */ 2431 MAIN_DIAGONAL: 10, 2432 /** 2433 * Reflection about X-axis. 2434 * @group groupD8 2435 * @type {GD8Symmetry} 2436 */ 2437 MIRROR_HORIZONTAL: 12, 2438 /** 2439 * Reflection about reverse diagonal. 2440 * @group groupD8 2441 * @type {GD8Symmetry} 2442 */ 2443 REVERSE_DIAGONAL: 14, 2444 /** 2445 * @group groupD8 2446 * @param {GD8Symmetry} ind - sprite rotation angle. 2447 * @returns {GD8Symmetry} The X-component of the U-axis 2448 * after rotating the axes. 2449 */ 2450 uX: (ind) => ux[ind], 2451 /** 2452 * @group groupD8 2453 * @param {GD8Symmetry} ind - sprite rotation angle. 2454 * @returns {GD8Symmetry} The Y-component of the U-axis 2455 * after rotating the axes. 2456 */ 2457 uY: (ind) => uy[ind], 2458 /** 2459 * @group groupD8 2460 * @param {GD8Symmetry} ind - sprite rotation angle. 2461 * @returns {GD8Symmetry} The X-component of the V-axis 2462 * after rotating the axes. 2463 */ 2464 vX: (ind) => vx[ind], 2465 /** 2466 * @group groupD8 2467 * @param {GD8Symmetry} ind - sprite rotation angle. 2468 * @returns {GD8Symmetry} The Y-component of the V-axis 2469 * after rotating the axes. 2470 */ 2471 vY: (ind) => vy[ind], 2472 /** 2473 * @group groupD8 2474 * @param {GD8Symmetry} rotation - symmetry whose opposite 2475 * is needed. Only rotations have opposite symmetries while 2476 * reflections don't. 2477 * @returns {GD8Symmetry} The opposite symmetry of `rotation` 2478 */ 2479 inv: (rotation) => { 2480 if (rotation & 8) { 2481 return rotation & 15; 2482 } 2483 return -rotation & 7; 2484 }, 2485 /** 2486 * Composes the two D8 operations. 2487 * 2488 * Taking `^` as reflection: 2489 * 2490 * | | E=0 | S=2 | W=4 | N=6 | E^=8 | S^=10 | W^=12 | N^=14 | 2491 * |-------|-----|-----|-----|-----|------|-------|-------|-------| 2492 * | E=0 | E | S | W | N | E^ | S^ | W^ | N^ | 2493 * | S=2 | S | W | N | E | S^ | W^ | N^ | E^ | 2494 * | W=4 | W | N | E | S | W^ | N^ | E^ | S^ | 2495 * | N=6 | N | E | S | W | N^ | E^ | S^ | W^ | 2496 * | E^=8 | E^ | N^ | W^ | S^ | E | N | W | S | 2497 * | S^=10 | S^ | E^ | N^ | W^ | S | E | N | W | 2498 * | W^=12 | W^ | S^ | E^ | N^ | W | S | E | N | 2499 * | N^=14 | N^ | W^ | S^ | E^ | N | W | S | E | 2500 * 2501 * [This is a Cayley table]{@link https://en.wikipedia.org/wiki/Cayley_table} 2502 * @group groupD8 2503 * @param {GD8Symmetry} rotationSecond - Second operation, which 2504 * is the row in the above cayley table. 2505 * @param {GD8Symmetry} rotationFirst - First operation, which 2506 * is the column in the above cayley table. 2507 * @returns {GD8Symmetry} Composed operation 2508 */ 2509 add: (rotationSecond, rotationFirst) => rotationCayley[rotationSecond][rotationFirst], 2510 /** 2511 * Reverse of `add`. 2512 * @group groupD8 2513 * @param {GD8Symmetry} rotationSecond - Second operation 2514 * @param {GD8Symmetry} rotationFirst - First operation 2515 * @returns {GD8Symmetry} Result 2516 */ 2517 sub: (rotationSecond, rotationFirst) => rotationCayley[rotationSecond][groupD8.inv(rotationFirst)], 2518 /** 2519 * Adds 180 degrees to rotation, which is a commutative 2520 * operation. 2521 * @group groupD8 2522 * @param {number} rotation - The number to rotate. 2523 * @returns {number} Rotated number 2524 */ 2525 rotate180: (rotation) => rotation ^ 4, 2526 /** 2527 * Checks if the rotation angle is vertical, i.e. south 2528 * or north. It doesn't work for reflections. 2529 * @group groupD8 2530 * @param {GD8Symmetry} rotation - The number to check. 2531 * @returns {boolean} Whether or not the direction is vertical 2532 */ 2533 isVertical: (rotation) => (rotation & 3) === 2, 2534 // rotation % 4 === 2 2535 /** 2536 * Approximates the vector `V(dx,dy)` into one of the 2537 * eight directions provided by `groupD8`. 2538 * @group groupD8 2539 * @param {number} dx - X-component of the vector 2540 * @param {number} dy - Y-component of the vector 2541 * @returns {GD8Symmetry} Approximation of the vector into 2542 * one of the eight symmetries. 2543 */ 2544 byDirection: (dx, dy) => { 2545 if (Math.abs(dx) * 2 <= Math.abs(dy)) { 2546 if (dy >= 0) { 2547 return groupD8.S; 2548 } 2549 return groupD8.N; 2550 } else if (Math.abs(dy) * 2 <= Math.abs(dx)) { 2551 if (dx > 0) { 2552 return groupD8.E; 2553 } 2554 return groupD8.W; 2555 } else if (dy > 0) { 2556 if (dx > 0) { 2557 return groupD8.SE; 2558 } 2559 return groupD8.SW; 2560 } else if (dx > 0) { 2561 return groupD8.NE; 2562 } 2563 return groupD8.NW; 2564 }, 2565 /** 2566 * Helps sprite to compensate texture packer rotation. 2567 * @group groupD8 2568 * @param {Matrix} matrix - sprite world matrix 2569 * @param {GD8Symmetry} rotation - The rotation factor to use. 2570 * @param {number} tx - sprite anchoring 2571 * @param {number} ty - sprite anchoring 2572 */ 2573 matrixAppendRotationInv: (matrix, rotation, tx = 0, ty = 0) => { 2574 const mat = rotationMatrices[groupD8.inv(rotation)]; 2575 mat.tx = tx; 2576 mat.ty = ty; 2577 matrix.append(mat); 2578 }, 2579 /** 2580 * Transforms rectangle coordinates based on texture packer rotation. 2581 * Used when texture atlas pages are rotated and coordinates need to be adju
2581sted. 2582 * @group groupD8 2583 * @param {RectangleLike} rect - Rectangle with original coordinates to transform 2584 * @param {RectangleLike} sourceFrame - Source texture frame (includes offset and dimensions) 2585 * @param {GD8Symmetry} rotation - The groupD8 rotation value 2586 * @param {Rectangle} out - Rectangle to store the result 2587 * @returns {Rectangle} Transformed coordinates (includes source frame offset) 2588 */ 2589 transformRectCoords: (rect, sourceFrame, rotation, out) => { 2590 const { x, y, width, height } = rect; 2591 const { x: frameX, y: frameY, width: frameWidth, height: frameHeight } = sourceFrame; 2592 if (rotation === groupD8.E) { 2593 out.set(x + frameX, y + frameY, width, height); 2594 return out; 2595 } else if (rotation === groupD8.S) { 2596 return out.set( 2597 frameWidth - y - height + frameX, 2598 x + frameY, 2599 height, 2600 width 2601 ); 2602 } else if (rotation === groupD8.W) { 2603 return out.set( 2604 frameWidth - x - width + frameX, 2605 frameHeight - y - height + frameY, 2606 width, 2607 height 2608 ); 2609 } else if (rotation === groupD8.N) { 2610 return out.set( 2611 y + frameX, 2612 frameHeight - x - width + frameY, 2613 height, 2614 width 2615 ); 2616 } 2617 return out.set(x + frameX, y + frameY, width, height); 2618 } 2619};
vendor: 9,439 bytes, lines 2620-2922
2620 2621const tempPoints = [new Point(), new Point(), new Point(), new Point()]; 2622class Rectangle { 2623 /** 2624 * @param x - The X coordinate of the upper-left corner of the rectangle 2625 * @param y - The Y coordinate of the upper-left corner of the rectangle 2626 * @param width - The overall width of the rectangle 2627 * @param height - The overall height of the rectangle 2628 */ 2629 constructor(x = 0, y = 0, width = 0, height = 0) { 2630 /** 2631 * The type of the object, mainly used to avoid `instanceof` checks 2632 * @example 2633 * ```ts 2634 * // Check shape type 2635 * const shape = new Rectangle(0, 0, 100, 100); 2636 * console.log(shape.type); // 'rectangle' 2637 * 2638 * // Use in type guards 2639 * if (shape.type === 'rectangle') { 2640 * console.log(shape.width, shape.height); 2641 * } 2642 * ``` 2643 * @readonly 2644 * @default 'rectangle' 2645 * @see {@link SHAPE_PRIMITIVE} For all shape types 2646 */ 2647 this.type = "rectangle"; 2648 this.x = Number(x); 2649 this.y = Number(y); 2650 this.width = Number(width); 2651 this.height = Number(height); 2652 } 2653 /** 2654 * Returns the left edge (x-coordinate) of the rectangle. 2655 * @example 2656 * ```ts 2657 * // Get left edge position 2658 * const rect = new Rectangle(100, 100, 200, 150); 2659 * console.log(rect.left); // 100 2660 * 2661 * // Use in alignment calculations 2662 * sprite.x = rect.left + padding; 2663 * 2664 * // Compare positions 2665 * if (point.x > rect.left) { 2666 * console.log('Point is right of rectangle'); 2667 * } 2668 * ``` 2669 * @readonly 2670 * @returns The x-coordinate of the left edge 2671 * @see {@link Rectangle.right} For right edge position 2672 * @see {@link Rectangle.x} For direct x-coordinate access 2673 */ 2674 get left() { 2675 return this.x; 2676 } 2677 /** 2678 * Returns the right edge (x + width) of the rectangle. 2679 * @example 2680 * ```ts 2681 * // Get right edge position 2682 * const rect = new Rectangle(100, 100, 200, 150); 2683 * console.log(rect.right); // 300 2684 * 2685 * // Align to right edge 2686 * sprite.x = rect.right - sprite.width; 2687 * 2688 * // Check boundaries 2689 * if (point.x < rect.right) { 2690 * console.log('Point is inside right bound'); 2691 * } 2692 * ``` 2693 * @readonly 2694 * @returns The x-coordinate of the right edge 2695 * @see {@link Rectangle.left} For left edge position 2696 * @see {@link Rectangle.width} For width value 2697 */ 2698 get right() { 2699 return this.x + this.width; 2700 } 2701 /** 2702 * Returns the top edge (y-coordinate) of the rectangle. 2703 * @example 2704 * ```ts 2705 * // Get top edge position 2706 * const rect = new Rectangle(100, 100, 200, 150); 2707 * console.log(rect.top); // 100 2708 * 2709 * // Position above rectangle 2710 * sprite.y = rect.top - sprite.height; 2711 * 2712 * // Check vertical position 2713 * if (point.y > rect.top) { 2714 * console.log('Point is below top edge'); 2715 * } 2716 * ``` 2717 * @readonly 2718 * @returns The y-coordinate of the top edge 2719 * @see {@link Rectangle.bottom} For bottom edge position 2720 * @see {@link Rectangle.y} For direct y-coordinate access 2721 */ 2722 get top() { 2723 return this.y; 2724 } 2725 /** 2726 * Returns the bottom edge (y + height) of the rectangle. 2727 * @example 2728 * ```ts 2729 * // Get bottom edge position 2730 * const rect = new Rectangle(100, 100, 200, 150); 2731 * console.log(rect.bottom); // 250 2732 * 2733 * // Stack below rectangle 2734 * sprite.y = rect.bottom + margin; 2735 * 2736 * // Check vertical bounds 2737 * if (point.y < rect.bottom) { 2738 * console.log('Point is above bottom edge'); 2739 * } 2740 * ``` 2741 * @readonly 2742 * @returns The y-coordinate of the bottom edge 2743 * @see {@link Rectangle.top} For top edge position 2744 * @see {@link Rectangle.height} For height value 2745 */ 2746 get bottom() { 2747 return this.y + this.height; 2748 } 2749 /** 2750 * Determines whether the Rectangle is empty (has no area). 2751 * @example 2752 * ```ts 2753 * // Check zero dimensions 2754 * const rect = new Rectangle(100, 100, 0, 50); 2755 * console.log(rect.isEmpty()); // true 2756 * ``` 2757 * @returns True if the rectangle has no area 2758 * @see {@link Rectangle.width} For width value 2759 * @see {@link Rectangle.height} For height value 2760 */ 2761 isEmpty() { 2762 return this.left === this.right || this.top === this.bottom; 2763 } 2764 /** 2765 * A constant empty rectangle. This is a new object every time the property is accessed. 2766 * @example 2767 * ```ts 2768 * // Get fresh empty rectangle 2769 * const empty = Rectangle.EMPTY; 2770 * console.log(empty.isEmpty()); // true 2771 * ``` 2772 * @returns A new empty rectangle instance 2773 * @see {@link Rectangle.isEmpty} For empty state testing 2774 */ 2775 static get EMPTY() { 2776 return new Rectangle(0, 0, 0, 0); 2777 } 2778 /** 2779 * Creates a clone of this Rectangle 2780 * @example 2781 * ```ts 2782 * // Basic cloning 2783 * const original = new Rectangle(100, 100, 200, 150); 2784 * const copy = original.clone(); 2785 * 2786 * // Clone and modify 2787 * const modified = original.clone(); 2788 * modified.width *= 2; 2789 * modified.height += 50; 2790 * 2791 * // Verify independence 2792 * console.log(original.width); // 200 2793 * console.log(modified.width); // 400 2794 * ``` 2795 * @returns A copy of the rectangle 2796 * @see {@link Rectangle.copyFrom} For copying into existing rectangle 2797 * @see {@link Rectangle.copyTo} For copying to another rectangle 2798 */ 2799 clone() { 2800 return new Rectangle(this.x, this.y, this.width, this.height); 2801 } 2802 /** 2803 * Converts a Bounds object to a Rectangle object. 2804 * @example 2805 * ```ts 2806 * // Convert bounds to rectangle 2807 * const bounds = container.getBounds(); 2808 * const rect = new Rectangle().copyFromBounds(bounds); 2809 * ``` 2810 * @param bounds - The bounds to copy and convert to a rectangle 2811 * @returns Returns itself 2812 * @see {@link Bounds} For bounds object structure 2813 * @see {@link Rectangle.getBounds} For getting rectangle bounds 2814 */ 2815 copyFromBounds(bounds) { 2816 this.x = bounds.minX; 2817 this.y = bounds.minY; 2818 this.width = bounds.maxX - bounds.minX; 2819 this.height = bounds.maxY - bounds.minY; 2820 return this; 2821 } 2822 /** 2823 * Copies another rectangle to this one. 2824 * @example 2825 * ```ts 2826 * // Basic copying 2827 * const source = new Rectangle(100, 100, 200, 150); 2828 * const target = new Rectangle(); 2829 * target.copyFrom(source); 2830 * 2831 * // Chain with other operations 2832 * const rect = new Rectangle() 2833 * .copyFrom(source) 2834 * .pad(10); 2835 * ``` 2836 * @param rectangle - The rectangle to copy from 2837 * @returns Returns itself 2838 * @see {@link Rectangle.copyTo} For copying to another rectangle 2839 * @see {@link Rectangle.clone} For creating new rectangle copy 2840 */ 2841 copyFrom(rectangle) { 2842 this.x = rectangle.x; 2843 this.y = rectangle.y; 2844 this.width = rectangle.width; 2845 this.height = rectangle.height; 2846 return this; 2847 } 2848 /** 2849 * Copies this rectangle to another one. 2850 * @example 2851 * ```ts 2852 * // Basic copying 2853 * const source = new Rectangle(100, 100, 200, 150); 2854 * const target = new Rectangle(); 2855 * source.copyTo(target); 2856 * 2857 * // Chain with other operations 2858 * const result = source 2859 * .copyTo(new Rectangle()) 2860 * .getBounds(); 2861 * ``` 2862 * @param rectangle - The rectangle to copy to 2863 * @returns Returns given parameter 2864 * @see {@link Rectangle.copyFrom} For copying from another rectangle 2865 * @see {@link Rectangle.clone} For creating new rectangle copy 2866 */ 2867 copyTo(rectangle) { 2868 rectangle.copyFrom(this); 2869 return rectangle; 2870 } 2871 /** 2872 * Checks whether the x and y coordinates given are contained within this Rectangle 2873 * @example 2874 * ```ts 2875 * // Basic containment check 2876 * const rect = new Rectangle(100, 100, 200, 150); 2877 * const isInside = rect.contains(150, 125); // true 2878 * // Check edge cases 2879 * console.log(rect.contains(100, 100)); // true (on edge) 2880 * console.log(rect.contains(300, 250)); // false (outside) 2881 * ``` 2882 * @param x - The X coordinate of the point to test 2883 * @param y - The Y coordinate of the point to test 2884 * @returns Whether the x/y coordinates are within this Rectangle 2885 * @see {@link Rectangle.containsRect} For rectangle containment 2886 * @see {@link Rectangle.strokeContains} For checking stroke intersection 2887 */ 2888 contains(x, y) { 2889 if (this.width <= 0 || this.height <= 0) { 2890 return false; 2891 } 2892 if (x >= this.x && x < this.x + this.width) { 2893 if (y >= this.y && y < this.y + this.height) { 2894 return true; 2895 } 2896 } 2897 return false; 2898 } 2899 /** 2900 * Checks whether the x and y coordinates given are contained within this rectangle including the stroke. 2901 * @example 2902 * ```ts 2903 * // Basic stroke check 2904 * const rect = new Rectangle(100, 100, 200, 150); 2905 * const isOnStroke = rect.strokeContains(150, 100, 4); // 4px line width 2906 * 2907 * // Check with different alignments 2908 * const innerStroke = rect.strokeContains(150, 100, 4, 1); // Inside 2909 * const centerStroke = rect.strokeContains(150, 100, 4, 0.5); // Centered 2910 * const outerStroke = rect.strokeContains(150, 100, 4, 0); // Outside 2911 * ``` 2912 * @param x - The X coordinate of the point to test 2913 * @param y - The Y coordinate of the point to test 2914 * @param strokeWidth - The width of the line to check 2915 * @param alignment - The alignment of the stroke (1 = inner, 0.5 = centered, 0 = outer) 2916 * @returns Whether the x/y coordinates are within this rectangle's stroke 2917 * @see {@link Rectangle.contains} For checking fill containment 2918 * @see {@link Rectangle.getBounds} For getting stroke bounds 2919 */ 2920 strokeContains(x, y, strokeWidth, alignment = 0.5) { 2921 const { width, height } = this; 2922 if (width <= 0 || height <= 0)
2922 2923
vendor: 11,306 bytes, lines 2923-3221
2923 return false; 2924 const _x = this.x; 2925 const _y = this.y; 2926 const strokeWidthOuter = strokeWidth * (1 - alignment); 2927 const strokeWidthInner = strokeWidth - strokeWidthOuter; 2928 const outerLeft = _x - strokeWidthOuter; 2929 const outerRight = _x + width + strokeWidthOuter; 2930 const outerTop = _y - strokeWidthOuter; 2931 const outerBottom = _y + height + strokeWidthOuter; 2932 const innerLeft = _x + strokeWidthInner; 2933 const innerRight = _x + width - strokeWidthInner; 2934 const innerTop = _y + strokeWidthInner; 2935 const innerBottom = _y + height - strokeWidthInner; 2936 return x >= outerLeft && x <= outerRight && y >= outerTop && y <= outerBottom && !(x > innerLeft && x < innerRight && y > innerTop && y < innerBottom); 2937 } 2938 /** 2939 * Determines whether the `other` Rectangle transformed by `transform` intersects with `this` Rectangle object. 2940 * Returns true only if the area of the intersection is >0, this means that Rectangles 2941 * sharing a side are not overlapping. Another side effect is that an arealess rectangle 2942 * (width or height equal to zero) can't intersect any other rectangle. 2943 * @param {Rectangle} other - The Rectangle to intersect with `this`. 2944 * @param {Matrix} transform - The transformation matrix of `other`. 2945 * @returns {boolean} A value of `true` if the transformed `other` Rectangle intersects with `this`; otherwise `false`. 2946 */ 2947 /** 2948 * Determines whether the `other` Rectangle transformed by `transform` intersects with `this` Rectangle object. 2949 * 2950 * Returns true only if the area of the intersection is greater than 0. 2951 * This means that rectangles sharing only a side are not considered intersecting. 2952 * @example 2953 * ```ts 2954 * // Basic intersection check 2955 * const rect1 = new Rectangle(0, 0, 100, 100); 2956 * const rect2 = new Rectangle(50, 50, 100, 100); 2957 * console.log(rect1.intersects(rect2)); // true 2958 * 2959 * // With transformation matrix 2960 * const matrix = new Matrix(); 2961 * matrix.rotate(Math.PI / 4); // 45 degrees 2962 * console.log(rect1.intersects(rect2, matrix)); // Checks with rotation 2963 * 2964 * // Edge cases 2965 * const zeroWidth = new Rectangle(0, 0, 0, 100); 2966 * console.log(rect1.intersects(zeroWidth)); // false (no area) 2967 * ``` 2968 * @remarks 2969 * - Returns true only if intersection area is > 0 2970 * - Rectangles sharing only a side are not intersecting 2971 * - Zero-area rectangles cannot intersect anything 2972 * - Supports optional transformation matrix 2973 * @param other - The Rectangle to intersect with `this` 2974 * @param transform - Optional transformation matrix of `other` 2975 * @returns True if the transformed `other` Rectangle intersects with `this` 2976 * @see {@link Rectangle.containsRect} For containment testing 2977 * @see {@link Rectangle.contains} For point testing 2978 */ 2979 intersects(other, transform) { 2980 if (!transform) { 2981 const x02 = this.x < other.x ? other.x : this.x; 2982 const x12 = this.right > other.right ? other.right : this.right; 2983 if (x12 <= x02) { 2984 return false; 2985 } 2986 const y02 = this.y < other.y ? other.y : this.y; 2987 const y12 = this.bottom > other.bottom ? other.bottom : this.bottom; 2988 return y12 > y02; 2989 } 2990 const x0 = this.left; 2991 const x1 = this.right; 2992 const y0 = this.top; 2993 const y1 = this.bottom; 2994 if (x1 <= x0 || y1 <= y0) { 2995 return false; 2996 } 2997 const lt = tempPoints[0].set(other.left, other.top); 2998 const lb = tempPoints[1].set(other.left, other.bottom); 2999 const rt = tempPoints[2].set(other.right, other.top); 3000 const rb = tempPoints[3].set(other.right, other.bottom); 3001 if (rt.x <= lt.x || lb.y <= lt.y) { 3002 return false; 3003 } 3004 const s = Math.sign(transform.a * transform.d - transform.b * transform.c); 3005 if (s === 0) { 3006 return false; 3007 } 3008 transform.apply(lt, lt); 3009 transform.apply(lb, lb); 3010 transform.apply(rt, rt); 3011 transform.apply(rb, rb); 3012 if (Math.max(lt.x, lb.x, rt.x, rb.x) <= x0 || Math.min(lt.x, lb.x, rt.x, rb.x) >= x1 || Math.max(lt.y, lb.y, rt.y, rb.y) <= y0 || Math.min(lt.y, lb.y, rt.y, rb.y) >= y1) { 3013 return false; 3014 } 3015 const nx = s * (lb.y - lt.y); 3016 const ny = s * (lt.x - lb.x); 3017 const n00 = nx * x0 + ny * y0; 3018 const n10 = nx * x1 + ny * y0; 3019 const n01 = nx * x0 + ny * y1; 3020 const n11 = nx * x1 + ny * y1; 3021 if (Math.max(n00, n10, n01, n11) <= nx * lt.x + ny * lt.y || Math.min(n00, n10, n01, n11) >= nx * rb.x + ny * rb.y) { 3022 return false; 3023 } 3024 const mx = s * (lt.y - rt.y); 3025 const my = s * (rt.x - lt.x); 3026 const m00 = mx * x0 + my * y0; 3027 const m10 = mx * x1 + my * y0; 3028 const m01 = mx * x0 + my * y1; 3029 const m11 = mx * x1 + my * y1; 3030 if (Math.max(m00, m10, m01, m11) <= mx * lt.x + my * lt.y || Math.min(m00, m10, m01, m11) >= mx * rb.x + my * rb.y) { 3031 return false; 3032 } 3033 return true; 3034 } 3035 /** 3036 * Pads the rectangle making it grow in all directions. 3037 * 3038 * If paddingY is omitted, both paddingX and paddingY will be set to paddingX. 3039 * @example 3040 * ```ts 3041 * // Basic padding 3042 * const rect = new Rectangle(100, 100, 200, 150); 3043 * rect.pad(10); // Adds 10px padding on all sides 3044 * 3045 * // Different horizontal and vertical padding 3046 * const uiRect = new Rectangle(0, 0, 100, 50); 3047 * uiRect.pad(20, 10); // 20px horizontal, 10px vertical 3048 * ``` 3049 * @remarks 3050 * - Adjusts x/y by subtracting padding 3051 * - Increases width/height by padding * 2 3052 * - Common in UI layout calculations 3053 * - Chainable with other methods 3054 * @param paddingX - The horizontal padding amount 3055 * @param paddingY - The vertical padding amount 3056 * @returns Returns itself 3057 * @see {@link Rectangle.enlarge} For growing to include another rectangle 3058 * @see {@link Rectangle.fit} For shrinking to fit within another rectangle 3059 */ 3060 pad(paddingX = 0, paddingY = paddingX) { 3061 this.x -= paddingX; 3062 this.y -= paddingY; 3063 this.width += paddingX * 2; 3064 this.height += paddingY * 2; 3065 return this; 3066 } 3067 /** 3068 * Fits this rectangle around the passed one. 3069 * @example 3070 * ```ts 3071 * // Basic fitting 3072 * const container = new Rectangle(0, 0, 100, 100); 3073 * const content = new Rectangle(25, 25, 200, 200); 3074 * content.fit(container); // Clips to container bounds 3075 * ``` 3076 * @param rectangle - The rectangle to fit around 3077 * @returns Returns itself 3078 * @see {@link Rectangle.enlarge} For growing to include another rectangle 3079 * @see {@link Rectangle.pad} For adding padding around the rectangle 3080 */ 3081 fit(rectangle) { 3082 const x1 = Math.max(this.x, rectangle.x); 3083 const x2 = Math.min(this.x + this.width, rectangle.x + rectangle.width); 3084 const y1 = Math.max(this.y, rectangle.y); 3085 const y2 = Math.min(this.y + this.height, rectangle.y + rectangle.height); 3086 this.x = x1; 3087 this.width = Math.max(x2 - x1, 0); 3088 this.y = y1; 3089 this.height = Math.max(y2 - y1, 0); 3090 return this; 3091 } 3092 /** 3093 * Enlarges rectangle so that its corners lie on a grid defined by resolution. 3094 * @example 3095 * ```ts 3096 * // Basic grid alignment 3097 * const rect = new Rectangle(10.2, 10.6, 100.8, 100.4); 3098 * rect.ceil(); // Aligns to whole pixels 3099 * 3100 * // Custom resolution grid 3101 * const uiRect = new Rectangle(5.3, 5.7, 50.2, 50.8); 3102 * uiRect.ceil(0.5); // Aligns to half pixels 3103 * 3104 * // Use with precision value 3105 * const preciseRect = new Rectangle(20.001, 20.999, 100.001, 100.999); 3106 * preciseRect.ceil(1, 0.01); // Handles small decimal variations 3107 * ``` 3108 * @param resolution - The grid size to align to (1 = whole pixels) 3109 * @param eps - Small number to prevent floating point errors 3110 * @returns Returns itself 3111 * @see {@link Rectangle.fit} For constraining to bounds 3112 * @see {@link Rectangle.enlarge} For growing dimensions 3113 */ 3114 ceil(resolution = 1, eps = 1e-3) { 3115 const x2 = Math.ceil((this.x + this.width - eps) * resolution) / resolution; 3116 const y2 = Math.ceil((this.y + this.height - eps) * resolution) / resolution; 3117 this.x = Math.floor((this.x + eps) * resolution) / resolution; 3118 this.y = Math.floor((this.y + eps) * resolution) / resolution; 3119 this.width = x2 - this.x; 3120 this.height = y2 - this.y; 3121 return this; 3122 } 3123 /** 3124 * Scales the rectangle's dimensions and position by the specified factors. 3125 * @example 3126 * ```ts 3127 * const rect = new Rectangle(50, 50, 100, 100); 3128 * 3129 * // Scale uniformly 3130 * rect.scale(0.5, 0.5); 3131 * // rect is now: x=25, y=25, width=50, height=50 3132 * 3133 * // non-uniformly 3134 * rect.scale(0.5, 1); 3135 * // rect is now: x=25, y=50, width=50, height=100 3136 * ``` 3137 * @param x - The factor by which to scale the horizontal properties (x, width). 3138 * @param y - The factor by which to scale the vertical properties (y, height). 3139 * @returns Returns itself 3140 */ 3141 scale(x, y = x) { 3142 this.x *= x; 3143 this.y *= y; 3144 this.width *= x; 3145 this.height *= y; 3146 return this; 3147 } 3148 /** 3149 * Enlarges this rectangle to include the passed rectangle. 3150 * @example 3151 * ```ts 3152 * // Basic enlargement 3153 * const rect = new Rectangle(50, 50, 100, 100); 3154 * const other = new Rectangle(0, 0, 200, 75); 3155 * rect.enlarge(other); 3156 * // rect is now: x=0, y=0, width=200, height=150 3157 * 3158 * // Use for bounding box calculation 3159 * const bounds = new Rectangle(); 3160 * objects.forEach((obj) => { 3161 * bounds.enlarge(obj.getBounds()); 3162 * }); 3163 * ``` 3164 * @param rectangle - The rectangle to include 3165 * @returns Returns itself 3166 * @see {@link Rectangle.fit} For shrinking to fit within another rectangle 3167 * @see {@link Rectangle.pad} For adding padding around the rectangle 3168 */ 3169 enlarge(rectangle) { 3170 const x1 = Math.min(this.x, rectangle.x); 3171 const x2 = Math.max(this.x + this.width, rectangle.x + rectangle.width); 3172 const y1 = Math.min(this.y, rectangle.y); 3173 const y2 = Math.max(this.y + this.height, rectangle.y + rectangle.height); 3174 this.x = x1; 3175 this.width = x2 - x1; 3176 this.y = y1; 3177 this.height = y2 - y1; 3178 return this; 3179 } 3180 /** 3181 * Returns the framing rectangle of the rectangle as a Rectangle object 3182 * @example 3183 * ```ts 3184 * // Basic bounds retrieval 3185 * const rect = new Rectangle(100, 100, 200, 150); 3186 * const bounds = rect.getBounds(); 3187 * 3188 * // Reuse existing rectangle 3189 * const out = new Rectangle(); 3190 * rect.getBounds(out); 3191 * ``` 3192 * @param out - Optional rectangle to store the result 3193 * @returns The framing rectangle 3194 * @see {@link Rectangle.copyFrom} For direct copying 3195 * @see {@link Rectangle.clone} For creating new copy 3196 */ 3197 getBounds(out) { 3198 out || (out = new Rectangle()); 3199 out.copyFrom(this); 3200 return out; 3201 } 3202 /** 3203 * Determines whether another Rectangle is fully contained within this Rectangle. 3204 * 3205 * Rectangles that occupy the same space are considered to be containing each other. 3206 * 3207 * Rectangles without area (width or height equal to zero) can't contain anything, 3208 * not even other arealess rectangles. 3209 * @example 3210 * ```ts 3211 * // Check if one rectangle contains another 3212 * const container = new Rectangle(0, 0, 100, 100); 3213 * const inner = new Rectangle(25, 25, 50, 50); 3214 * 3215 * console.log(container.containsRect(inner)); // true 3216 * 3217 * // Check overlapping rectangles 3218 * const partial = new Rectangle(75, 75, 50, 50); 3219 * console.log(container.containsRect(partial)); // false 3220 * 3221 * // Zero-area rectangles
3221 3222 * const empty = new Rectangle(0, 0, 0, 100); 3223 * console.log(container.containsRect(empty)); // false 3224 * ```
3225 * @param other - The Rectangle to check for containment 3226 * @returns True if other is fully contained within this Rectangle 3227 * @see {@link Rectangle.contains} For point containment 3228 * @see {@link Rectangle.intersects} For overlap testing 3229 */ 3230 containsRect(other) { 3231 if (this.width <= 0 || this.height <= 0) 3232 return false; 3233 const x1 = other.x; 3234 const y1 = other.y; 3235 const x2 = other.x + other.width; 3236 const y2 = other.y + other.height; 3237 return x1 >= this.x && x1 < this.x + this.width && y1 >= this.y && y1 < this.y + this.height && x2 >= this.x && x2 < this.x + this.width && y2 >= this.y && y2 < this.y + this.height; 3238 } 3239 /** 3240 * Sets the position and dimensions of the rectangle. 3241 * @example 3242 * ```ts 3243 * // Basic usage 3244 * const rect = new Rectangle(); 3245 * rect.set(100, 100, 200, 150); 3246 * 3247 * // Chain with other operations 3248 * const bounds = new Rectangle() 3249 * .set(0, 0, 100, 100) 3250 * .pad(10); 3251 * ``` 3252 * @param x - The X coordinate of the upper-left corner of the rectangle 3253 * @param y - The Y coordinate of the upper-left corner of the rectangle 3254 * @param width - The overall width of the rectangle 3255 * @param height - The overall height of the rectangle 3256 * @returns Returns itself for method chaining 3257 * @see {@link Rectangle.copyFrom} For copying from another rectangle 3258 * @see {@link Rectangle.clone} For creating a new copy 3259 */ 3260 set(x, y, width, height) { 3261 this.x = x; 3262 this.y = y; 3263 this.width = width; 3264 this.height = height; 3265 return this; 3266 } 3267 toString() { 3268 return `[pixi.js/math:Rectangle x=${this.x} y=${this.y} width=${this.width} height=${this.height}]`; 3269 } 3270} 3271 3272const uidCache = { 3273 default: -1 3274}; 3275function uid(name = "default") { 3276 if (uidCache[name] === void 0) { 3277 uidCache[name] = -1; 3278 } 3279 return ++uidCache[name]; 3280} 3281 3282const warnings = /* @__PURE__ */ new Set(); 3283const v8_0_0 = "8.0.0"; 3284const v8_3_4 = "8.3.4"; 3285const deprecationState = { 3286 quiet: false, 3287 noColor: false 3288}; 3289const deprecation = (version, message, ignoreDepth = 3) => { 3290 if (deprecationState.quiet || warnings.has(message)) 3291 return; 3292 let stack = new Error().stack; 3293 const deprecationMessage = `${message} 3294Deprecated since v${version}`; 3295 const useGroup = typeof console.groupCollapsed === "function" && !deprecationState.noColor; 3296 if (typeof stack === "undefined") { 3297 console.warn("PixiJS Deprecation Warning: ", deprecationMessage); 3298 } else { 3299 stack = stack.split("\n").splice(ignoreDepth).join("\n"); 3300 if (useGroup) { 3301 console.groupCollapsed( 3302 "%cPixiJS Deprecation Warning: %c%s", 3303 "color:#614108;background:#fffbe6", 3304 "font-weight:normal;color:#614108;background:#fffbe6", 3305 deprecationMessage 3306 ); 3307 console.warn(stack); 3308 console.groupEnd(); 3309 } else { 3310 console.warn("PixiJS Deprecation Warning: ", deprecationMessage); 3311 console.warn(stack); 3312 } 3313 } 3314 warnings.add(message); 3315}; 3316Object.defineProperties(deprecation, { 3317 quiet: { 3318 get: () => deprecationState.quiet, 3319 set: (value) => { 3320 deprecationState.quiet = value; 3321 }, 3322 enumerable: true, 3323 configurable: false 3324 }, 3325 noColor: { 3326 get: () => deprecationState.noColor, 3327 set: (value) => { 3328 deprecationState.noColor = value; 3329 }, 3330 enumerable: true, 3331 configurable: false 3332 } 3333}); 3334 3335const NOOP = () => { 3336}; 3337 3338function nextPow2(v) { 3339 v += v === 0 ? 1 : 0; 3340 --v; 3341 v |= v >>> 1; 3342 v |= v >>> 2; 3343 v |= v >>> 4; 3344 v |= v >>> 8; 3345 v |= v >>> 16; 3346 return v + 1; 3347} 3348function isPow2(v) { 3349 return !(v & v - 1) && !!v; 3350} 3351 3352function definedProps(obj) { 3353 const result = {}; 3354 for (const key in obj) { 3355 if (obj[key] !== void 0) { 3356 result[key] = obj[key]; 3357 } 3358 } 3359 return result; 3360} 3361 3362const idHash$1 = /* @__PURE__ */ Object.create(null); 3363function createResourceIdFromString(value) { 3364 const id = idHash$1[value]; 3365 if (id === void 0) { 3366 idHash$1[value] = uid("resource"); 3367 } 3368 return id; 3369} 3370const _TextureStyle = class _TextureStyle extends EventEmitter { 3371 /** 3372 * @param options - options for the style 3373 */ 3374 constructor(options = {}) { 3375 super(); 3376 /** @internal */ 3377 this._resourceType = "textureSampler"; 3378 /** @internal */ 3379 this._touched = 0; 3380 /** 3381 * Specifies the maximum anisotropy value clamp used by the sampler.
3382 * Note: Most implementations support {@link TextureStyle#maxAnisotropy} values in range 3383 * between 1 and 16, inclusive. The used value of {@link TextureStyle#maxAnisotropy} will 3384 * be clamped to the maximum value that the platform supports. 3385 * @internal 3386 */ 3387 this._maxAnisotropy = 1; 3388 /** 3389 * Has the style been destroyed? 3390 * @readonly 3391 */ 3392 this.destroyed = false; 3393 options = { ..._TextureStyle.defaultOptions, ...options }; 3394 this.addressMode = options.addressMode; 3395 this.addressModeU = options.addressModeU ?? this.addressModeU; 3396 this.addressModeV = options.addressModeV ?? this.addressModeV; 3397 this.addressModeW = options.addressModeW ?? this.addressModeW; 3398 this.scaleMode = options.scaleMode; 3399 this.magFilter = options.magFilter ?? this.magFilter; 3400 this.minFilter = options.minFilter ?? this.minFilter; 3401 this.mipmapFilter = options.mipmapFilter ?? this.mipmapFilter; 3402 this.lodMinClamp = options.lodMinClamp; 3403 this.lodMaxClamp = options.lodMaxClamp; 3404 this.compare = options.compare; 3405 this.maxAnisotropy = options.maxAnisotropy ?? 1; 3406 } 3407 set addressMode(value) { 3408 this.addressModeU = value; 3409 this.addressModeV = value; 3410 this.addressModeW = value; 3411 } 3412 /** setting this will set wrapModeU,wrapModeV and wrapModeW all at once! */ 3413 get addressMode() { 3414 return this.addressModeU; 3415 } 3416 set wrapMode(value) { 3417 deprecation(v8_0_0, "TextureStyle.wrapMode is now TextureStyle.addressMode"); 3418 this.addressMode = value; 3419 } 3420 get wrapMode() { 3421 return this.addressMode; 3422 } 3423 set scaleMode(value) { 3424 this.magFilter = value; 3425 this.minFilter = value; 3426 this.mipmapFilter = value; 3427 } 3428 /** setting this will set magFilter,minFilter and mipmapFilter all at once! */ 3429 get scaleMode() { 3430 return this.magFilter; 3431 } 3432 /** Specifies the maximum anisotropy value clamp used by the sampler. */ 3433 set maxAnisotropy(value) { 3434 this._maxAnisotropy = Math.min(value, 16); 3435 if (this._maxAnisotropy > 1) { 3436 this.scaleMode = "linear"; 3437 } 3438 } 3439 get maxAnisotropy() { 3440 return this._maxAnisotropy; 3441 } 3442 // TODO - move this to WebGL? 3443 get _resourceId() { 3444 return this._sharedResourceId || this._generateResourceId(); 3445 } 3446 update() { 3447 this._sharedResourceId = null; 3448 this.emit("change", this); 3449 } 3450 _generateResourceId() { 3451 const bigKey = `${this.addressModeU}-${this.addressModeV}-${this.addressModeW}-${this.magFilter}-${this.minFilter}-${this.mipmapFilter}-${this.lodMinClamp}-${this.lodMaxClamp}-${this.compare}-${this._maxAnisotropy}`; 3452 this._sharedResourceId = createResourceIdFromString(bigKey); 3453 return this._resourceId; 3454 } 3455 /** Destroys the style */ 3456 destroy() { 3457 this.destroyed = true; 3458 this.emit("destroy", this); 3459 this.emit("change", this); 3460 this.removeAllListeners(); 3461 } 3462}; 3463/** default options for the style */ 3464_TextureStyle.defaultOptions = { 3465 addressMode: "clamp-to-edge", 3466 scaleMode: "linear" 3467}; 3468let TextureStyle = _TextureStyle; 3469 3470const _TextureSource = class _TextureSource extends EventEmitter { 3471 /** 3472 * @param options - options for creating a new TextureSource 3473 */ 3474 constructor(options = {}) { 3475 super(); 3476 this.options = options; 3477 /** @internal */ 3478 this._gpuData = /* @__PURE__ */ Object.create(null); 3479 /** @internal */ 3480 this._gcLastUsed = -1; 3481 /** unique id for this Texture source */ 3482 this.uid = uid("textureSource"); 3483 /** 3484 * The resource type used by this TextureSource. This is used by the bind groups to determine 3485 * how to handle this resource. 3486 * @internal 3487 */ 3488 this._resourceType = "textureSource"; 3489 /** 3490 * i unique resource id, used by the bind group systems. 3491 * This can change if the texture is resized or its resource changes 3492 * @internal 3493 */ 3494 this._resourceId = uid("resource"); 3495 /** 3496 * this is how the backends know how to upload this texture to the GPU 3497 * It changes depending on the resource type. Classes that extend TextureSource 3498 * should override this property. 3499 * @internal 3500 */ 3501 this.uploadMethodId = "unknown"; 3502 /** @internal */ 3503 this._resolution = 1; 3504 /** the pixel width of this texture source. This is the REAL pure number, not accounting resolution */ 3505 this.pixelWidth = 1; 3506 /** the pixel height of this texture source. This is the REAL pure number, not accounting resolution */ 3507 this.pixelHeight = 1; 3508 /** 3509 * the width of this texture source, accounting for resolution 3510 * eg pixelWidth 200, resolution 2, then width will be 100 3511 */ 3512 this.width = 1; 3513 /** 3514 * the height of this texture source, accounting for resolution 3515 * eg pixelHeight 200, resolution 2, then height will be 100 3516 */ 3517 this.height = 1; 3518 /** 3519 * The number of samples of a multisample texture. This is always 1 for non-multisample textures. 3520 * To enable multisample for a texture, set antialias to true 3521 * @internal 3522 */ 3523 this.sampleCount = 1; 3524 /** The number of mip levels to generate for this texture. this is overridden if autoGenerateMipmaps is true */ 3525 this.mipLevelCount = 1; 3526 /** 3527 * Should we auto generate mipmaps for this texture? This will automatically generate mipmaps 3528 * for this texture when uploading to the GPU. Mipmapped textures take up more memory, but 3529 * can look better when scaled down. 3530 * 3531 * For performance reasons, it is recommended to NOT use this with RenderTextures, as they are often updated every frame. 3532 * If you do, make sure to call `updateMipmaps` after you update the texture. 3533 */ 3534 this.autoGenerateMipmaps = false;
3535 /** the format that the texture data has */ 3536 this.format = "rgba8unorm"; 3537 /** how many dimensions does this texture have? currently v8 only supports 2d */ 3538 this.dimension = "2d"; 3539 /** 3540 * Only really affects RenderTextures. 3541 * Should we use antialiasing for this texture. It will look better, but may impact performance as a 3542 * Blit operation will be required to resolve the texture. 3543 */ 3544 this.antialias = false; 3545 /** 3546 * Used by automatic texture Garbage Collection, stores last GC tick when it was bound 3547 * @protected 3548 */ 3549 this._touched = 0; 3550 /** 3551 * Used by the batcher to build texture batches. faster to have the variable here! 3552 * @protected 3553 */ 3554 this._batchTick = -1; 3555 /** 3556 * A temporary batch location for the texture batching. Here for performance reasons only! 3557 * @protected 3558 */ 3559 this._textureBindLocation = -1; 3560 options = { ..._TextureSource.defaultOptions, ...options }; 3561 this.label = options.label ?? ""; 3562 this.resource = options.resource; 3563 this.autoGarbageCollect = options.autoGarbageCollect; 3564 this._resolution = options.resolution; 3565 if (options.width) { 3566 this.pixelWidth = options.width * this._resolution; 3567 } else { 3568 this.pixelWidth = this.resource ? this.resourceWidth ?? 1 : 1; 3569 } 3570 if (options.height) { 3571 this.pixelHeight = options.height * this._resolution; 3572 } else { 3573 this.pixelHeight = this.resource ? this.resourceHeight ?? 1 : 1; 3574 } 3575 this.width = this.pixelWidth / this._resolution; 3576 this.height = this.pixelHeight / this._resolution; 3577 this.format = options.format; 3578 this.dimension = options.dimensions; 3579 this.mipLevelCount = options.mipLevelCount; 3580 this.autoGenerateMipmaps = options.autoGenerateMipmaps; 3581 this.sampleCount = options.sampleCount; 3582 this.antialias = options.antialias; 3583 this.alphaMode = options.alphaMode; 3584 this.style = new TextureStyle(definedProps(options)); 3585 this.destroyed = false; 3586 this._refreshPOT(); 3587 } 3588 /** returns itself */ 3589 get source() { 3590 return this; 3591 } 3592 /** the style of the texture */ 3593 get style() { 3594 return this._style; 3595 } 3596 set style(value) { 3597 if (this.style === value) 3598 return; 3599 this._style?.off("change", this._onStyleChange, this); 3600 this._style = value; 3601 this._style?.on("change", this._onStyleChange, this); 3602 this._onStyleChange(); 3603 } 3604 /** Specifies the maximum anisotropy value clamp used by the sampler. */ 3605 set maxAnisotropy(value) { 3606 this._style.maxAnisotropy = value; 3607 } 3608 get maxAnisotropy() { 3609 return this._style.maxAnisotropy; 3610 } 3611 /** setting this will set wrapModeU, wrapModeV and wrapModeW all at once! */ 3612 get addressMode() { 3613 return this._style.addressMode; 3614 } 3615 set addressMode(value) { 3616 this._style.addressMode = value; 3617 } 3618 /** setting this will set wrapModeU, wrapModeV and wrapModeW all at once! */ 3619 get repeatMode() { 3620 return this._style.addressMode; 3621 } 3622 set repeatMode(value) { 3623 this._style.addressMode = value; 3624 } 3625 /** Specifies the sampling behavior when the sample footprint is smaller than or equal to one texel. */ 3626 get magFilter() { 3627 return this._style.magFilter; 3628 } 3629 set magFilter(value) { 3630 this._style.magFilter = value; 3631 } 3632 /** Specifies the sampling behavior when the sample footprint is larger than one texel. */ 3633 get minFilter() { 3634 return this._style.minFilter; 3635 } 3636 set minFilter(value) { 3637 this._style.minFilter = value; 3638 } 3639 /** Specifies behavior for sampling between mipmap levels. */ 3640 get mipmapFilter() { 3641 return this._style.mipmapFilter; 3642 } 3643 set mipmapFilter(value) { 3644 this._style.mipmapFilter = value; 3645 } 3646 /** Specifies the minimum and maximum levels of detail, respectively, used internally when sampling a texture. */ 3647 get lodMinClamp() { 3648 return this._style.lodMinClamp; 3649 } 3650 set lodMinClamp(value) { 3651 this._style.lodMinClamp = value; 3652 } 3653 /** Specifies the minimum and maximum levels of detail, respectively, used internally when sampling a texture. */ 3654 get lodMaxClamp() { 3655 return this._style.lodMaxClamp; 3656 } 3657 set lodMaxClamp(value) { 3658 this._style.lodMaxClamp = value; 3659 } 3660 _onStyleChange() {
vendor: 6,885 bytes, lines 3661-3858
3661 this.emit("styleChange", this); 3662 } 3663 /** call this if you have modified the texture outside of the constructor */ 3664 update() { 3665 if (this.resource) { 3666 const resolution = this._resolution; 3667 const didResize = this.resize(this.resourceWidth / resolution, this.resourceHeight / resolution); 3668 if (didResize) 3669 return; 3670 } 3671 this.emit("update", this); 3672 } 3673 /** Destroys this texture source */ 3674 destroy() { 3675 this.destroyed = true; 3676 this.unload(); 3677 this.emit("destroy", this); 3678 if (this._style) { 3679 this._style.destroy(); 3680 this._style = null; 3681 } 3682 this.uploadMethodId = null; 3683 this.resource = null; 3684 this.removeAllListeners(); 3685 } 3686 /** 3687 * This will unload the Texture source from the GPU. This will free up the GPU memory 3688 * As soon as it is required fore rendering, it will be re-uploaded. 3689 */ 3690 unload() { 3691 this._resourceId = uid("resource"); 3692 this.emit("change", this); 3693 this.emit("unload", this); 3694 for (const key in this._gpuData) { 3695 this._gpuData[key]?.destroy?.(); 3696 } 3697 this._gpuData = /* @__PURE__ */ Object.create(null); 3698 } 3699 /** the width of the resource. This is the REAL pure number, not accounting resolution */ 3700 get resourceWidth() { 3701 const { resource } = this; 3702 return resource.naturalWidth || resource.videoWidth || resource.displayWidth || resource.width; 3703 } 3704 /** the height of the resource. This is the REAL pure number, not accounting resolution */ 3705 get resourceHeight() { 3706 const { resource } = this; 3707 return resource.naturalHeight || resource.videoHeight || resource.displayHeight || resource.height; 3708 } 3709 /** 3710 * the resolution of the texture. Changing this number, will not change the number of pixels in the actual texture 3711 * but will the size of the texture when rendered. 3712 * 3713 * changing the resolution of this texture to 2 for example will make it appear twice as small when rendered (as pixel 3714 * density will have increased) 3715 */ 3716 get resolution() { 3717 return this._resolution; 3718 } 3719 set resolution(resolution) { 3720 if (this._resolution === resolution) 3721 return; 3722 this._resolution = resolution; 3723 this.width = this.pixelWidth / resolution; 3724 this.height = this.pixelHeight / resolution; 3725 } 3726 /** 3727 * Resize the texture, this is handy if you want to use the texture as a render texture 3728 * @param width - the new width of the texture 3729 * @param height - the new height of the texture 3730 * @param resolution - the new resolution of the texture 3731 * @returns - if the texture was resized 3732 */ 3733 resize(width, height, resolution) { 3734 resolution || (resolution = this._resolution); 3735 width || (width = this.width); 3736 height || (height = this.height); 3737 const newPixelWidth = Math.round(width * resolution); 3738 const newPixelHeight = Math.round(height * resolution); 3739 this.width = newPixelWidth / resolution; 3740 this.height = newPixelHeight / resolution; 3741 this._resolution = resolution; 3742 if (this.pixelWidth === newPixelWidth && this.pixelHeight === newPixelHeight) { 3743 return false; 3744 } 3745 this._refreshPOT(); 3746 this.pixelWidth = newPixelWidth; 3747 this.pixelHeight = newPixelHeight; 3748 this.emit("resize", this); 3749 this._resourceId = uid("resource"); 3750 this.emit("change", this); 3751 return true; 3752 } 3753 /** 3754 * Lets the renderer know that this texture has been updated and its mipmaps should be re-generated. 3755 * This is only important for RenderTexture instances, as standard Texture instances will have their 3756 * mipmaps generated on upload. You should call this method after you make any change to the texture 3757 * 3758 * The reason for this is is can be quite expensive to update mipmaps for a texture. So by default, 3759 * We want you, the developer to specify when this action should happen. 3760 * 3761 * Generally you don't want to have mipmaps generated on Render targets that are changed every frame, 3762 */ 3763 updateMipmaps() { 3764 if (this.autoGenerateMipmaps && this.mipLevelCount > 1) { 3765 this.emit("updateMipmaps", this); 3766 } 3767 } 3768 set wrapMode(value) { 3769 this._style.wrapMode = value; 3770 } 3771 get wrapMode() { 3772 return this._style.wrapMode; 3773 } 3774 set scaleMode(value) { 3775 this._style.scaleMode = value; 3776 } 3777 /** setting this will set magFilter,minFilter and mipmapFilter all at once! */ 3778 get scaleMode() { 3779 return this._style.scaleMode; 3780 } 3781 /** 3782 * Refresh check for isPowerOfTwo texture based on size 3783 * @private 3784 */ 3785 _refreshPOT() { 3786 this.isPowerOfTwo = isPow2(this.pixelWidth) && isPow2(this.pixelHeight); 3787 } 3788 static test(_resource) { 3789 throw new Error("Unimplemented"); 3790 } 3791}; 3792/** The default options used when creating a new TextureSource. override these to add your own defaults */ 3793_TextureSource.defaultOptions = { 3794 resolution: 1, 3795 format: "bgra8unorm", 3796 alphaMode: "premultiply-alpha-on-upload", 3797 dimensions: "2d", 3798 mipLevelCount: 1, 3799 autoGenerateMipmaps: false, 3800 sampleCount: 1, 3801 antialias: false, 3802 autoGarbageCollect: false 3803}; 3804let TextureSource = _TextureSource; 3805 3806class BufferImageSource extends TextureSource { 3807 constructor(options) { 3808 const buffer = options.resource || new Float32Array(options.width * options.height * 4); 3809 let format = options.format; 3810 if (!format) { 3811 if (buffer instanceof Float32Array) { 3812 format = "rgba32float"; 3813 } else if (buffer instanceof Int32Array) { 3814 format = "rgba32uint"; 3815 } else if (buffer instanceof Uint32Array) { 3816 format = "rgba32uint"; 3817 } else if (buffer instanceof Int16Array) { 3818 format = "rgba16uint"; 3819 } else if (buffer instanceof Uint16Array) { 3820 format = "rgba16uint"; 3821 } else if (buffer instanceof Int8Array) { 3822 format = "bgra8unorm"; 3823 } else { 3824 format = "bgra8unorm"; 3825 } 3826 } 3827 super({ 3828 ...options, 3829 resource: buffer, 3830 format 3831 }); 3832 this.uploadMethodId = "buffer"; 3833 } 3834 static test(resource) { 3835 return resource instanceof Int8Array || resource instanceof Uint8Array || resource instanceof Uint8ClampedArray || resource instanceof Int16Array || resource instanceof Uint16Array || resource instanceof Int32Array || resource instanceof Uint32Array || resource instanceof Float32Array; 3836 } 3837} 3838BufferImageSource.extension = ExtensionType.TextureSource; 3839 3840const tempMat = new Matrix(); 3841class TextureMatrix { 3842 /** 3843 * @param texture - observed texture 3844 * @param clampMargin - Changes frame clamping, 0.5 by default. Use -0.5 for extra border. 3845 */ 3846 constructor(texture, clampMargin) { 3847 this.mapCoord = new Matrix(); 3848 this.uClampFrame = new Float32Array(4); 3849 this.uClampOffset = new Float32Array(2); 3850 this._textureID = -1; 3851 this._updateID = 0; 3852 this.clampOffset = 0; 3853 if (typeof clampMargin === "undefined") { 3854 this.clampMargin = texture.width < 10 ? 0 : 0.5; 3855 } else { 3856 this.clampMargin = clampMargin; 3857 } 3858 this.isSimple = false;
3859 this.texture = texture; 3860 } 3861 /** Texture property. */ 3862 get texture() { 3863 return this._texture; 3864 } 3865 set texture(value) { 3866 if (this.texture === value) 3867 return; 3868 this._texture?.removeListener("update", this.update, this); 3869 this._texture = value; 3870 this._texture.addListener("update", this.update, this); 3871 this.update(); 3872 } 3873 /** 3874 * Multiplies uvs array to transform 3875 * @param uvs - mesh uvs 3876 * @param [out=uvs] - output 3877 * @returns - output 3878 */ 3879 multiplyUvs(uvs, out) { 3880 if (out === void 0) { 3881 out = uvs; 3882 } 3883 const mat = this.mapCoord; 3884 for (let i = 0; i < uvs.length; i += 2) { 3885 const x = uvs[i]; 3886 const y = uvs[i + 1]; 3887 out[i] = x * mat.a + y * mat.c + mat.tx; 3888 out[i + 1] = x * mat.b + y * mat.d + mat.ty; 3889 } 3890 return out; 3891 } 3892 /** 3893 * Updates matrices if texture was changed 3894 * @returns - whether or not it was updated 3895 */ 3896 update() { 3897 const tex = this._texture; 3898 this._updateID++; 3899 const uvs = tex.uvs; 3900 this.mapCoord.set(uvs.x1 - uvs.x0, uvs.y1 - uvs.y0, uvs.x3 - uvs.x0, uvs.y3 - uvs.y0, uvs.x0, uvs.y0); 3901 const orig = tex.orig; 3902 const trim = tex.trim; 3903 if (trim) { 3904 tempMat.set( 3905 orig.width / trim.width, 3906 0, 3907 0, 3908 orig.height / trim.height, 3909 -trim.x / trim.width, 3910 -trim.y / trim.height 3911 ); 3912 this.mapCoord.append(tempMat); 3913 } 3914 const texBase = tex.source; 3915 const frame = this.uClampFrame; 3916 const margin = this.clampMargin / texBase._resolution; 3917 const offset = this.clampOffset / texBase._resolution; 3918 frame[0] = (tex.frame.x + margin + offset) / texBase.width; 3919 frame[1] = (tex.frame.y + margin + offset) / texBase.height; 3920 frame[2] = (tex.frame.x + tex.frame.width - margin + offset) / texBase.width; 3921 frame[3] = (tex.frame.y + tex.frame.height - margin + offset) / texBase.height; 3922 this.uClampOffset[0] = this.clampOffset / texBase.pixelWidth; 3923 this.uClampOffset[1] = this.clampOffset / texBase.pixelHeight; 3924 this.isSimple = tex.frame.width === texBase.width && tex.frame.height === texBase.height && tex.rotate === 0; 3925 return true; 3926 } 3927} 3928 3929class Texture extends EventEmitter { 3930 /** 3931 * @param {TextureOptions} options - Options for the texture 3932 */ 3933 constructor({ 3934 source, 3935 label, 3936 frame, 3937 orig, 3938 trim, 3939 defaultAnchor, 3940 defaultBorders, 3941 rotate, 3942 dynamic 3943 } = {}) { 3944 super(); 3945 /** unique id for this texture */ 3946 this.uid = uid("texture"); 3947 /** A uvs object based on the given frame and the texture source */ 3948 this.uvs = { x0: 0, y0: 0, x1: 0, y1: 0, x2: 0, y2: 0, x3: 0, y3: 0 }; 3949 /** 3950 * This is the area of the BaseTexture image to actually copy to the Canvas / WebGL when rendering, 3951 * irrespective of the actual frame size or placement (which can be influenced by trimmed texture atlases) 3952 */ 3953 this.frame = new Rectangle(); 3954 /** 3955 * Does this Texture have any frame data assigned to it? 3956 * 3957 * This mode is enabled automatically if no frame was passed inside constructor. 3958 * 3959 * In this mode texture is subscribed to baseTexture events, and fires `update` on any change. 3960 * 3961 * Beware, after loading or resize of baseTexture event can fired two times! 3962 * If you want more control, subscribe on baseTexture itself. 3963 * @example 3964 * texture.on('update', () => {}); 3965 */ 3966 this.noFrame = false; 3967 /** 3968 * Set to true if you plan on modifying the uvs of this texture. 3969 * When this is the case, sprites and other objects using the texture will 3970 * make sure to listen for changes to the uvs and update their vertices accordingly. 3971 */ 3972 this.dynamic = false; 3973 /** is it a texture? yes! used for type checking */ 3974 this.isTexture = true; 3975 this.label = label; 3976 this.source = source?.source ?? new TextureSource(); 3977 this.noFrame = !frame; 3978 if (frame) { 3979 this.frame.copyFrom(frame); 3980 } else { 3981 const { width, height } = this._source; 3982 this.frame.width = width; 3983 this.frame.height = height; 3984 } 3985 this.orig = orig || this.frame; 3986 this.trim = trim; 3987 this.rotate = rotate ?? 0; 3988 this.defaultAnchor = defaultAnchor; 3989 this.defaultBorders = defaultBorders; 3990 this.destroyed = false;
vendor: 17,032 bytes, lines 3991-4555
3991 this.dynamic = dynamic || false; 3992 this.updateUvs(); 3993 } 3994 set source(value) { 3995 if (this._source) { 3996 this._source.off("resize", this.update, this); 3997 } 3998 this._source = value; 3999 value.on("resize", this.update, this); 4000 this.emit("update", this); 4001 } 4002 /** the underlying source of the texture (equivalent of baseTexture in v7) */ 4003 get source() { 4004 return this._source; 4005 } 4006 /** returns a TextureMatrix instance for this texture. By default, that object is not created because its heavy. */ 4007 get textureMatrix() { 4008 if (!this._textureMatrix) { 4009 this._textureMatrix = new TextureMatrix(this); 4010 } 4011 return this._textureMatrix; 4012 } 4013 /** The width of the Texture in pixels. */ 4014 get width() { 4015 return this.orig.width; 4016 } 4017 /** The height of the Texture in pixels. */ 4018 get height() { 4019 return this.orig.height; 4020 } 4021 /** Call this function when you have modified the frame of this texture. */ 4022 updateUvs() { 4023 const { uvs, frame } = this; 4024 const { width, height } = this._source; 4025 const nX = frame.x / width; 4026 const nY = frame.y / height; 4027 const nW = frame.width / width; 4028 const nH = frame.height / height; 4029 let rotate = this.rotate; 4030 if (rotate) { 4031 const w2 = nW / 2; 4032 const h2 = nH / 2; 4033 const cX = nX + w2; 4034 const cY = nY + h2; 4035 rotate = groupD8.add(rotate, groupD8.NW); 4036 uvs.x0 = cX + w2 * groupD8.uX(rotate); 4037 uvs.y0 = cY + h2 * groupD8.uY(rotate); 4038 rotate = groupD8.add(rotate, 2); 4039 uvs.x1 = cX + w2 * groupD8.uX(rotate); 4040 uvs.y1 = cY + h2 * groupD8.uY(rotate); 4041 rotate = groupD8.add(rotate, 2); 4042 uvs.x2 = cX + w2 * groupD8.uX(rotate); 4043 uvs.y2 = cY + h2 * groupD8.uY(rotate); 4044 rotate = groupD8.add(rotate, 2); 4045 uvs.x3 = cX + w2 * groupD8.uX(rotate); 4046 uvs.y3 = cY + h2 * groupD8.uY(rotate); 4047 } else { 4048 uvs.x0 = nX; 4049 uvs.y0 = nY; 4050 uvs.x1 = nX + nW; 4051 uvs.y1 = nY; 4052 uvs.x2 = nX + nW; 4053 uvs.y2 = nY + nH; 4054 uvs.x3 = nX; 4055 uvs.y3 = nY + nH; 4056 } 4057 } 4058 /** 4059 * Destroys this texture 4060 * @param destroySource - Destroy the source when the texture is destroyed. 4061 */ 4062 destroy(destroySource = false) { 4063 if (this._source) { 4064 this._source.off("resize", this.update, this); 4065 if (destroySource) { 4066 this._source.destroy(); 4067 this._source = null; 4068 } 4069 } 4070 this._textureMatrix = null; 4071 this.destroyed = true; 4072 this.emit("destroy", this); 4073 this.removeAllListeners(); 4074 } 4075 /** 4076 * Call this if you have modified the `texture outside` of the constructor. 4077 * 4078 * If you have modified this texture's source, you must separately call `texture.source.update()` to see those changes. 4079 */ 4080 update() { 4081 if (this.noFrame) { 4082 this.frame.width = this._source.width; 4083 this.frame.height = this._source.height; 4084 } 4085 this.updateUvs(); 4086 this.emit("update", this); 4087 } 4088 /** @deprecated since 8.0.0 */ 4089 get baseTexture() { 4090 deprecation(v8_0_0, "Texture.baseTexture is now Texture.source"); 4091 return this._source; 4092 } 4093} 4094Texture.EMPTY = new Texture({ 4095 label: "EMPTY", 4096 source: new TextureSource({ 4097 label: "EMPTY" 4098 }) 4099}); 4100Texture.EMPTY.destroy = NOOP; 4101Texture.WHITE = new Texture({ 4102 source: new BufferImageSource({ 4103 resource: new Uint8Array([255, 255, 255, 255]), 4104 width: 1, 4105 height: 1, 4106 alphaMode: "premultiply-alpha-on-upload", 4107 label: "WHITE" 4108 }), 4109 label: "WHITE" 4110}); 4111Texture.WHITE.destroy = NOOP; 4112 4113function updateQuadBounds(bounds, anchor, texture) { 4114 const { width, height } = texture.orig; 4115 const trim = texture.trim; 4116 if (trim) { 4117 const sourceWidth = trim.width; 4118 const sourceHeight = trim.height; 4119 bounds.minX = trim.x - anchor._x * width; 4120 bounds.maxX = bounds.minX + sourceWidth; 4121 bounds.minY = trim.y - anchor._y * height; 4122 bounds.maxY = bounds.minY + sourceHeight; 4123 } else { 4124 bounds.minX = -anchor._x * width; 4125 bounds.maxX = bounds.minX + width; 4126 bounds.minY = -anchor._y * height; 4127 bounds.maxY = bounds.minY + height; 4128 } 4129} 4130 4131const defaultMatrix = new Matrix(); 4132class Bounds { 4133 /** 4134 * Creates a new Bounds object. 4135 * @param minX - The minimum X coordinate of the bounds. 4136 * @param minY - The minimum Y coordinate of the bounds. 4137 * @param maxX - The maximum X coordinate of the bounds. 4138 * @param maxY - The maximum Y coordinate of the bounds. 4139 */ 4140 constructor(minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity) { 4141 /** 4142 * The minimum X coordinate of the bounds. 4143 * Represents the leftmost edge of the bounding box. 4144 * @example 4145 * ```ts 4146 * const bounds = new Bounds(); 4147 * // Set left edge 4148 * bounds.minX = 100; 4149 * ``` 4150 * @default Infinity 4151 */ 4152 this.minX = Infinity; 4153 /** 4154 * The minimum Y coordinate of the bounds. 4155 * Represents the topmost edge of the bounding box. 4156 * @example 4157 * ```ts 4158 * const bounds = new Bounds(); 4159 * // Set top edge 4160 * bounds.minY = 100; 4161 * ``` 4162 * @default Infinity 4163 */ 4164 this.minY = Infinity; 4165 /** 4166 * The maximum X coordinate of the bounds. 4167 * Represents the rightmost edge of the bounding box. 4168 * @example 4169 * ```ts 4170 * const bounds = new Bounds(); 4171 * // Set right edge 4172 * bounds.maxX = 200; 4173 * // Get width 4174 * const width = bounds.maxX - bounds.minX; 4175 * ``` 4176 * @default -Infinity 4177 */ 4178 this.maxX = -Infinity; 4179 /** 4180 * The maximum Y coordinate of the bounds. 4181 * Represents the bottommost edge of the bounding box. 4182 * @example 4183 * ```ts 4184 * const bounds = new Bounds(); 4185 * // Set bottom edge 4186 * bounds.maxY = 200; 4187 * // Get height 4188 * const height = bounds.maxY - bounds.minY; 4189 * ``` 4190 * @default -Infinity 4191 */ 4192 this.maxY = -Infinity; 4193 /** 4194 * The transformation matrix applied to this bounds object. 4195 * Used when calculating bounds with transforms. 4196 * @example 4197 * ```ts 4198 * const bounds = new Bounds(); 4199 * 4200 * // Apply translation matrix 4201 * bounds.matrix = new Matrix() 4202 * .translate(100, 100); 4203 * 4204 * // Combine transformations 4205 * bounds.matrix = new Matrix() 4206 * .translate(50, 50) 4207 * .rotate(Math.PI / 4) 4208 * .scale(2, 2); 4209 * 4210 * // Use in bounds calculations 4211 * bounds.addFrame(0, 0, 100, 100); // Uses current matrix 4212 * bounds.addFrame(0, 0, 100, 100, customMatrix); // Override matrix 4213 * ``` 4214 * @advanced 4215 */ 4216 this.matrix = defaultMatrix; 4217 this.minX = minX; 4218 this.minY = minY; 4219 this.maxX = maxX; 4220 this.maxY = maxY; 4221 } 4222 /** 4223 * Checks if bounds are empty, meaning either width or height is zero or negative. 4224 * Empty bounds occur when min values exceed max values on either axis. 4225 * @example 4226 * ```ts 4227 * const bounds = new Bounds(); 4228 * 4229 * // Check if newly created bounds are empty 4230 * console.log(bounds.isEmpty()); // true, default bounds are empty 4231 * 4232 * // Add frame and check again 4233 * bounds.addFrame(0, 0, 100, 100); 4234 * console.log(bounds.isEmpty()); // false, bounds now have area 4235 * 4236 * // Clear bounds 4237 * bounds.clear(); 4238 * console.log(bounds.isEmpty()); // true, bounds are empty again 4239 * ``` 4240 * @returns True if bounds are empty (have no area) 4241 * @see {@link Bounds#clear} For resetting bounds 4242 * @see {@link Bounds#isValid} For checking validity 4243 */ 4244 isEmpty() { 4245 return this.minX > this.maxX || this.minY > this.maxY; 4246 } 4247 /** 4248 * The bounding rectangle representation of these bounds. 4249 * Lazily creates and updates a Rectangle instance based on the current bounds. 4250 * @example 4251 * ```ts 4252 * const bounds = new Bounds(0, 0, 100, 100); 4253 * 4254 * // Get rectangle representation 4255 * const rect = bounds.rectangle; 4256 * console.log(rect.x, rect.y, rect.width, rect.height); 4257 * 4258 * // Use for hit testing 4259 * if (bounds.rectangle.contains(mouseX, mouseY)) { 4260 * console.log('Mouse is inside bounds!'); 4261 * } 4262 * ``` 4263 * @see {@link Rectangle} For rectangle methods 4264 * @see {@link Bounds.isEmpty} For bounds validation 4265 */ 4266 get rectangle() { 4267 if (!this._rectangle) { 4268 this._rectangle = new Rectangle(); 4269 } 4270 const rectangle = this._rectangle; 4271 if (this.minX > this.maxX || this.minY > this.maxY) { 4272 rectangle.x = 0; 4273 rectangle.y = 0; 4274 rectangle.width = 0; 4275 rectangle.height = 0; 4276 } else { 4277 rectangle.copyFromBounds(this); 4278 } 4279 return rectangle; 4280 } 4281 /** 4282 * Clears the bounds and resets all coordinates to their default values. 4283 * Resets the transformation matrix back to identity. 4284 * @example 4285 * ```ts 4286 * const bounds = new Bounds(0, 0, 100, 100); 4287 * console.log(bounds.isEmpty()); // false 4288 * // Clear the bounds 4289 * bounds.clear(); 4290 * console.log(bounds.isEmpty()); // true 4291 * ``` 4292 * @returns This bounds object for chaining 4293 */ 4294 clear() { 4295 this.minX = Infinity; 4296 this.minY = Infinity; 4297 this.maxX = -Infinity; 4298 this.maxY = -Infinity; 4299 this.matrix = defaultMatrix; 4300 return this; 4301 } 4302 /** 4303 * Sets the bounds directly using coordinate values. 4304 * Provides a way to set all bounds values at once. 4305 * @example 4306 * ```ts 4307 * const bounds = new Bounds(); 4308 * bounds.set(0, 0, 100, 100); 4309 * ``` 4310 * @param x0 - Left X coordinate of frame 4311 * @param y0 - Top Y coordinate of frame 4312 * @param x1 - Right X coordinate of frame 4313 * @param y1 - Bottom Y coordinate of frame 4314 * @see {@link Bounds#addFrame} For matrix-aware bounds setting 4315 * @see {@link Bounds#clear} For resetting bounds 4316 */ 4317 set(x0, y0, x1, y1) { 4318 this.minX = x0; 4319 this.minY = y0; 4320 this.maxX = x1; 4321 this.maxY = y1; 4322 } 4323 /** 4324 * Adds a rectangular frame to the bounds, optionally transformed by a matrix. 4325 * Updates the bounds to encompass the new frame coordinates. 4326 * @example 4327 * ```ts 4328 * const bounds = new Bounds(); 4329 * bounds.addFrame(0, 0, 100, 100); 4330 * 4331 * // Add transformed frame 4332 * const matrix = new Matrix() 4333 * .translate(50, 50) 4334 * .rotate(Math.PI / 4); 4335 * bounds.addFrame(0, 0, 100, 100, matrix); 4336 * ``` 4337 * @param x0 - Left X coordinate of frame 4338 * @param y0 - Top Y coordinate of frame 4339 * @param x1 - Right X coordinate of frame 4340 * @param y1 - Bottom Y coordinate of frame 4341 * @param matrix - Optional transformation matrix 4342 * @see {@link Bounds#addRect} For adding Rectangle objects 4343 * @see {@link Bounds#addBounds} For adding other Bounds 4344 */ 4345 addFrame(x0, y0, x1, y1, matrix) { 4346 matrix || (matrix = this.matrix); 4347 const a = matrix.a; 4348 const b = matrix.b; 4349 const c = matrix.c; 4350 const d = matrix.d; 4351 const tx = matrix.tx; 4352 const ty = matrix.ty; 4353 let minX = this.minX; 4354 let minY = this.minY; 4355 let maxX = this.maxX; 4356 let maxY = this.maxY; 4357 let x = a * x0 + c * y0 + tx; 4358 let y = b * x0 + d * y0 + ty; 4359 if (x < minX) 4360 minX = x; 4361 if (y < minY) 4362 minY = y; 4363 if (x > maxX) 4364 maxX = x; 4365 if (y > maxY) 4366 maxY = y; 4367 x = a * x1 + c * y0 + tx; 4368 y = b * x1 + d * y0 + ty; 4369 if (x < minX) 4370 minX = x; 4371 if (y < minY) 4372 minY = y; 4373 if (x > maxX) 4374 maxX = x; 4375 if (y > maxY) 4376 maxY = y; 4377 x = a * x0 + c * y1 + tx; 4378 y = b * x0 + d * y1 + ty; 4379 if (x < minX) 4380 minX = x; 4381 if (y < minY) 4382 minY = y; 4383 if (x > maxX) 4384 maxX = x; 4385 if (y > maxY) 4386 maxY = y; 4387 x = a * x1 + c * y1 + tx; 4388 y = b * x1 + d * y1 + ty; 4389 if (x < minX) 4390 minX = x; 4391 if (y < minY) 4392 minY = y; 4393 if (x > maxX) 4394 maxX = x; 4395 if (y > maxY) 4396 maxY = y; 4397 this.minX = minX; 4398 this.minY = minY; 4399 this.maxX = maxX; 4400 this.maxY = maxY; 4401 } 4402 /** 4403 * Adds a rectangle to the bounds, optionally transformed by a matrix. 4404 * Updates the bounds to encompass the given rectangle. 4405 * @example 4406 * ```ts 4407 * const bounds = new Bounds(); 4408 * // Add simple rectangle 4409 * const rect = new Rectangle(0, 0, 100, 100); 4410 * bounds.addRect(rect); 4411 * 4412 * // Add transformed rectangle 4413 * const matrix = new Matrix() 4414 * .translate(50, 50) 4415 * .rotate(Math.PI / 4); 4416 * bounds.addRect(rect, matrix); 4417 * ``` 4418 * @param rect - The rectangle to be added 4419 * @param matrix - Optional transformation matrix 4420 * @see {@link Bounds#addFrame} For adding raw coordinates 4421 * @see {@link Bounds#addBounds} For adding other bounds 4422 */ 4423 addRect(rect, matrix) { 4424 this.addFrame(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, matrix); 4425 } 4426 /** 4427 * Adds another bounds object to this one, optionally transformed by a matrix. 4428 * Expands the bounds to include the given bounds' area. 4429 * @example 4430 * ```ts 4431 * const bounds = new Bounds(); 4432 * 4433 * // Add child bounds 4434 * const childBounds = sprite.getBounds(); 4435 * bounds.addBounds(childBounds); 4436 * 4437 * // Add transformed bounds 4438 * const matrix = new Matrix() 4439 * .scale(2, 2); 4440 * bounds.addBounds(childBounds, matrix); 4441 * ``` 4442 * @param bounds - The bounds to be added 4443 * @param matrix - Optional transformation matrix 4444 * @see {@link Bounds#addFrame} For adding raw coordinates 4445 * @see {@link Bounds#addRect} For adding rectangles 4446 */ 4447 addBounds(bounds, matrix) { 4448 this.addFrame(bounds.minX, bounds.minY, bounds.maxX, bounds.maxY, matrix); 4449 } 4450 /** 4451 * Adds other Bounds as a mask, creating an intersection of the two bounds. 4452 * Only keeps the overlapping region between current bounds and mask bounds. 4453 * @example 4454 * ```ts 4455 * const bounds = new Bounds(0, 0, 100, 100); 4456 * // Create mask bounds 4457 * const mask = new Bounds(); 4458 * mask.addFrame(50, 50, 150, 150); 4459 * // Apply mask - results in bounds of (50,50,100,100) 4460 * bounds.addBoundsMask(mask); 4461 * ``` 4462 * @param mask - The Bounds to use as a mask 4463 * @see {@link Bounds#addBounds} For union operation 4464 * @see {@link Bounds#fit} For fitting to rectangle 4465 */ 4466 addBoundsMask(mask) { 4467 this.minX = this.minX > mask.minX ? this.minX : mask.minX; 4468 this.minY = this.minY > mask.minY ? this.minY : mask.minY; 4469 this.maxX = this.maxX < mask.maxX ? this.maxX : mask.maxX; 4470 this.maxY = this.maxY < mask.maxY ? this.maxY : mask.maxY; 4471 } 4472 /** 4473 * Applies a transformation matrix to the bounds, updating its coordinates. 4474 * Transforms all corners of the bounds using the given matrix. 4475 * @example 4476 * ```ts 4477 * const bounds = new Bounds(0, 0, 100, 100); 4478 * // Apply translation 4479 * const translateMatrix = new Matrix() 4480 * .translate(50, 50); 4481 * bounds.applyMatrix(translateMatrix); 4482 * ``` 4483 * @param matrix - The matrix to apply to the bounds 4484 * @see {@link Matrix} For matrix operations 4485 * @see {@link Bounds#addFrame} For adding transformed frames 4486 */ 4487 applyMatrix(matrix) { 4488 const minX = this.minX; 4489 const minY = this.minY; 4490 const maxX = this.maxX; 4491 const maxY = this.maxY; 4492 const { a, b, c, d, tx, ty } = matrix; 4493 let x = a * minX + c * minY + tx; 4494 let y = b * minX + d * minY + ty; 4495 this.minX = x; 4496 this.minY = y; 4497 this.maxX = x; 4498 this.maxY = y; 4499 x = a * maxX + c * minY + tx; 4500 y = b * maxX + d * minY + ty; 4501 this.minX = x < this.minX ? x : this.minX; 4502 this.minY = y < this.minY ? y : this.minY; 4503 this.maxX = x > this.maxX ? x : this.maxX; 4504 this.maxY = y > this.maxY ? y : this.maxY; 4505 x = a * minX + c * maxY + tx; 4506 y = b * minX + d * maxY + ty; 4507 this.minX = x < this.minX ? x : this.minX; 4508 this.minY = y < this.minY ? y : this.minY; 4509 this.maxX = x > this.maxX ? x : this.maxX; 4510 this.maxY = y > this.maxY ? y : this.maxY; 4511 x = a * maxX + c * maxY + tx; 4512 y = b * maxX + d * maxY + ty; 4513 this.minX = x < this.minX ? x : this.minX; 4514 this.minY = y < this.minY ? y : this.minY; 4515 this.maxX = x > this.maxX ? x : this.maxX; 4516 this.maxY = y > this.maxY ? y : this.maxY; 4517 } 4518 /** 4519 * Resizes the bounds object to fit within the given rectangle. 4520 * Clips the bounds if they extend beyond the rectangle's edges. 4521 * @example 4522 * ```ts 4523 * const bounds = new Bounds(0, 0, 200, 200); 4524 * // Fit within viewport 4525 * const viewport = new Rectangle(50, 50, 100, 100); 4526 * bounds.fit(viewport); 4527 * // bounds are now (50, 50, 150, 150) 4528 * ``` 4529 * @param rect - The rectangle to fit within 4530 * @returns This bounds object for chaining 4531 * @see {@link Bounds#addBoundsMask} For intersection 4532 * @see {@link Bounds#pad} For expanding bounds 4533 */ 4534 fit(rect) { 4535 if (this.minX < rect.left) 4536 this.minX = rect.left; 4537 if (this.maxX > rect.right) 4538 this.maxX = rect.right; 4539 if (this.minY < rect.top) 4540 this.minY = rect.top; 4541 if (this.maxY > rect.bottom) 4542 this.maxY = rect.bottom; 4543 return this; 4544 } 4545 /** 4546 * Resizes the bounds object to include the given bounds. 4547 * Similar to fit() but works with raw coordinate values instead of a Rectangle. 4548 * @example 4549 * ```ts 4550 * const bounds = new Bounds(0, 0, 200, 200); 4551 * // Fit to specific coordinates 4552 * bounds.fitBounds(50, 150, 50, 150); 4553 * // bounds are now (50, 50, 150, 150) 4554 * ``` 4555 * @param left - The left value of the bounds
4556 * @param right - The right value of the bounds 4557 * @param top - The top value of the bounds 4558 * @param bottom - The bottom value of the bounds 4559 * @returns This bounds object for chaining 4560 * @see {@link Bounds#fit} For fitting to Rectangle 4561 * @see {@link Bounds#addBoundsMask} For intersection 4562 */ 4563 fitBounds(left, right, top, bottom) { 4564 if (this.minX < left) 4565 this.minX = left; 4566 if (this.maxX > right) 4567 this.maxX = right; 4568 if (this.minY < top) 4569 this.minY = top; 4570 if (this.maxY > bottom) 4571
vendor: 11,590 bytes, lines 4571-4975
4571 this.maxY = bottom; 4572 return this; 4573 } 4574 /** 4575 * Pads bounds object, making it grow in all directions. 4576 * If paddingY is omitted, both paddingX and paddingY will be set to paddingX. 4577 * @example 4578 * ```ts 4579 * const bounds = new Bounds(0, 0, 100, 100); 4580 * 4581 * // Add equal padding 4582 * bounds.pad(10); 4583 * // bounds are now (-10, -10, 110, 110) 4584 * 4585 * // Add different padding for x and y 4586 * bounds.pad(20, 10); 4587 * // bounds are now (-30, -20, 130, 120) 4588 * ``` 4589 * @param paddingX - The horizontal padding amount 4590 * @param paddingY - The vertical padding amount 4591 * @returns This bounds object for chaining 4592 * @see {@link Bounds#fit} For constraining bounds 4593 * @see {@link Bounds#scale} For uniform scaling 4594 */ 4595 pad(paddingX, paddingY = paddingX) { 4596 this.minX -= paddingX; 4597 this.maxX += paddingX; 4598 this.minY -= paddingY; 4599 this.maxY += paddingY; 4600 return this; 4601 } 4602 /** 4603 * Ceils the bounds by rounding up max values and rounding down min values. 4604 * Useful for pixel-perfect calculations and avoiding fractional pixels. 4605 * @example 4606 * ```ts 4607 * const bounds = new Bounds(); 4608 * bounds.set(10.2, 10.9, 50.1, 50.8); 4609 * 4610 * // Round to whole pixels 4611 * bounds.ceil(); 4612 * // bounds are now (10, 10, 51, 51) 4613 * ``` 4614 * @returns This bounds object for chaining 4615 * @see {@link Bounds#scale} For size adjustments 4616 * @see {@link Bounds#fit} For constraining bounds 4617 */ 4618 ceil() { 4619 this.minX = Math.floor(this.minX); 4620 this.minY = Math.floor(this.minY); 4621 this.maxX = Math.ceil(this.maxX); 4622 this.maxY = Math.ceil(this.maxY); 4623 return this; 4624 } 4625 /** 4626 * Creates a new Bounds instance with the same values. 4627 * @example 4628 * ```ts 4629 * const bounds = new Bounds(0, 0, 100, 100); 4630 * 4631 * // Create a copy 4632 * const copy = bounds.clone(); 4633 * 4634 * // Original and copy are independent 4635 * bounds.pad(10); 4636 * console.log(copy.width === bounds.width); // false 4637 * ``` 4638 * @returns A new Bounds instance with the same values 4639 * @see {@link Bounds#copyFrom} For reusing existing bounds 4640 */ 4641 clone() { 4642 return new Bounds(this.minX, this.minY, this.maxX, this.maxY); 4643 } 4644 /** 4645 * Scales the bounds by the given values, adjusting all edges proportionally. 4646 * @example 4647 * ```ts 4648 * const bounds = new Bounds(0, 0, 100, 100); 4649 * 4650 * // Scale uniformly 4651 * bounds.scale(2); 4652 * // bounds are now (0, 0, 200, 200) 4653 * 4654 * // Scale non-uniformly 4655 * bounds.scale(0.5, 2); 4656 * // bounds are now (0, 0, 100, 400) 4657 * ``` 4658 * @param x - The X value to scale by 4659 * @param y - The Y value to scale by (defaults to x) 4660 * @returns This bounds object for chaining 4661 * @see {@link Bounds#pad} For adding padding 4662 * @see {@link Bounds#fit} For constraining size 4663 */ 4664 scale(x, y = x) { 4665 this.minX *= x; 4666 this.minY *= y; 4667 this.maxX *= x; 4668 this.maxY *= y; 4669 return this; 4670 } 4671 /** 4672 * The x position of the bounds in local space. 4673 * Setting this value will move the bounds while maintaining its width. 4674 * @example 4675 * ```ts 4676 * const bounds = new Bounds(0, 0, 100, 100); 4677 * // Get x position 4678 * console.log(bounds.x); // 0 4679 * 4680 * // Move bounds horizontally 4681 * bounds.x = 50; 4682 * console.log(bounds.minX, bounds.maxX); // 50, 150 4683 * 4684 * // Width stays the same 4685 * console.log(bounds.width); // Still 100 4686 * ``` 4687 */ 4688 get x() { 4689 return this.minX; 4690 } 4691 set x(value) { 4692 const width = this.maxX - this.minX; 4693 this.minX = value; 4694 this.maxX = value + width; 4695 } 4696 /** 4697 * The y position of the bounds in local space. 4698 * Setting this value will move the bounds while maintaining its height. 4699 * @example 4700 * ```ts 4701 * const bounds = new Bounds(0, 0, 100, 100); 4702 * // Get y position 4703 * console.log(bounds.y); // 0 4704 * 4705 * // Move bounds vertically 4706 * bounds.y = 50; 4707 * console.log(bounds.minY, bounds.maxY); // 50, 150 4708 * 4709 * // Height stays the same 4710 * console.log(bounds.height); // Still 100 4711 * ``` 4712 */ 4713 get y() { 4714 return this.minY; 4715 } 4716 set y(value) { 4717 const height = this.maxY - this.minY; 4718 this.minY = value; 4719 this.maxY = value + height; 4720 } 4721 /** 4722 * The width value of the bounds. 4723 * Represents the distance between minX and maxX coordinates. 4724 * @example 4725 * ```ts 4726 * const bounds = new Bounds(0, 0, 100, 100); 4727 * // Get width 4728 * console.log(bounds.width); // 100 4729 * // Resize width 4730 * bounds.width = 200; 4731 * console.log(bounds.maxX - bounds.minX); // 200 4732 * ``` 4733 */ 4734 get width() { 4735 return this.maxX - this.minX; 4736 } 4737 set width(value) { 4738 this.maxX = this.minX + value; 4739 } 4740 /** 4741 * The height value of the bounds. 4742 * Represents the distance between minY and maxY coordinates. 4743 * @example 4744 * ```ts 4745 * const bounds = new Bounds(0, 0, 100, 100); 4746 * // Get height 4747 * console.log(bounds.height); // 100 4748 * // Resize height 4749 * bounds.height = 150; 4750 * console.log(bounds.maxY - bounds.minY); // 150 4751 * ``` 4752 */ 4753 get height() { 4754 return this.maxY - this.minY; 4755 } 4756 set height(value) { 4757 this.maxY = this.minY + value; 4758 } 4759 /** 4760 * The left edge coordinate of the bounds. 4761 * Alias for minX. 4762 * @example 4763 * ```ts 4764 * const bounds = new Bounds(50, 0, 150, 100); 4765 * console.log(bounds.left); // 50 4766 * console.log(bounds.left === bounds.minX); // true 4767 * ``` 4768 * @readonly 4769 */ 4770 get left() { 4771 return this.minX; 4772 } 4773 /** 4774 * The right edge coordinate of the bounds. 4775 * Alias for maxX. 4776 * @example 4777 * ```ts 4778 * const bounds = new Bounds(0, 0, 100, 100); 4779 * console.log(bounds.right); // 100 4780 * console.log(bounds.right === bounds.maxX); // true 4781 * ``` 4782 * @readonly 4783 */ 4784 get right() { 4785 return this.maxX; 4786 } 4787 /** 4788 * The top edge coordinate of the bounds. 4789 * Alias for minY. 4790 * @example 4791 * ```ts 4792 * const bounds = new Bounds(0, 25, 100, 125); 4793 * console.log(bounds.top); // 25 4794 * console.log(bounds.top === bounds.minY); // true 4795 * ``` 4796 * @readonly 4797 */ 4798 get top() { 4799 return this.minY; 4800 } 4801 /** 4802 * The bottom edge coordinate of the bounds. 4803 * Alias for maxY. 4804 * @example 4805 * ```ts 4806 * const bounds = new Bounds(0, 0, 100, 200); 4807 * console.log(bounds.bottom); // 200 4808 * console.log(bounds.bottom === bounds.maxY); // true 4809 * ``` 4810 * @readonly 4811 */ 4812 get bottom() { 4813 return this.maxY; 4814 } 4815 /** 4816 * Whether the bounds has positive width and height. 4817 * Checks if both dimensions are greater than zero. 4818 * @example 4819 * ```ts 4820 * const bounds = new Bounds(0, 0, 100, 100); 4821 * // Check if bounds are positive 4822 * console.log(bounds.isPositive); // true 4823 * 4824 * // Negative bounds 4825 * bounds.maxX = bounds.minX; 4826 * console.log(bounds.isPositive); // false, width is 0 4827 * ``` 4828 * @readonly 4829 * @see {@link Bounds#isEmpty} For checking empty state 4830 * @see {@link Bounds#isValid} For checking validity 4831 */ 4832 get isPositive() { 4833 return this.maxX - this.minX > 0 && this.maxY - this.minY > 0; 4834 } 4835 /** 4836 * Whether the bounds has valid coordinates. 4837 * Checks if the bounds has been initialized with real values. 4838 * @example 4839 * ```ts 4840 * const bounds = new Bounds(); 4841 * console.log(bounds.isValid); // false, default state 4842 * 4843 * // Set valid bounds 4844 * bounds.addFrame(0, 0, 100, 100); 4845 * console.log(bounds.isValid); // true 4846 * ``` 4847 * @readonly 4848 * @see {@link Bounds#isEmpty} For checking empty state 4849 * @see {@link Bounds#isPositive} For checking dimensions 4850 */ 4851 get isValid() { 4852 return this.minX + this.minY !== Infinity; 4853 } 4854 /** 4855 * Adds vertices from a Float32Array to the bounds, optionally transformed by a matrix. 4856 * Used for efficiently updating bounds from raw vertex data. 4857 * @example 4858 * ```ts 4859 * const bounds = new Bounds(); 4860 * 4861 * // Add vertices from geometry 4862 * const vertices = new Float32Array([ 4863 * 0, 0, // Vertex 1 4864 * 100, 0, // Vertex 2 4865 * 100, 100 // Vertex 3 4866 * ]); 4867 * bounds.addVertexData(vertices, 0, 6); 4868 * 4869 * // Add transformed vertices 4870 * const matrix = new Matrix() 4871 * .translate(50, 50) 4872 * .rotate(Math.PI / 4); 4873 * bounds.addVertexData(vertices, 0, 6, matrix); 4874 * 4875 * // Add subset of vertices 4876 * bounds.addVertexData(vertices, 2, 4); // Only second vertex 4877 * ``` 4878 * @param vertexData - The array of vertices to add 4879 * @param beginOffset - Starting index in the vertex array 4880 * @param endOffset - Ending index in the vertex array (excluded) 4881 * @param matrix - Optional transformation matrix 4882 * @see {@link Bounds#addFrame} For adding rectangular frames 4883 * @see {@link Matrix} For transformation details 4884 */ 4885 addVertexData(vertexData, beginOffset, endOffset, matrix) { 4886 let minX = this.minX; 4887 let minY = this.minY; 4888 let maxX = this.maxX; 4889 let maxY = this.maxY; 4890 matrix || (matrix = this.matrix); 4891 const a = matrix.a; 4892 const b = matrix.b; 4893 const c = matrix.c; 4894 const d = matrix.d; 4895 const tx = matrix.tx; 4896 const ty = matrix.ty; 4897 for (let i = beginOffset; i < endOffset; i += 2) { 4898 const localX = vertexData[i]; 4899 const localY = vertexData[i + 1]; 4900 const x = a * localX + c * localY + tx; 4901 const y = b * localX + d * localY + ty; 4902 minX = x < minX ? x : minX; 4903 minY = y < minY ? y : minY; 4904 maxX = x > maxX ? x : maxX; 4905 maxY = y > maxY ? y : maxY; 4906 } 4907 this.minX = minX; 4908 this.minY = minY; 4909 this.maxX = maxX; 4910 this.maxY = maxY; 4911 } 4912 /** 4913 * Checks if a point is contained within the bounds. 4914 * Returns true if the point's coordinates fall within the bounds' area. 4915 * @example 4916 * ```ts 4917 * const bounds = new Bounds(0, 0, 100, 100); 4918 * // Basic point check 4919 * console.log(bounds.containsPoint(50, 50)); // true 4920 * console.log(bounds.containsPoint(150, 150)); // false 4921 * 4922 * // Check edges 4923 * console.log(bounds.containsPoint(0, 0)); // true, includes edges 4924 * console.log(bounds.containsPoint(100, 100)); // true, includes edges 4925 * ``` 4926 * @param x - x coordinate to check 4927 * @param y - y coordinate to check 4928 * @returns True if the point is inside the bounds 4929 * @see {@link Bounds#isPositive} For valid bounds check 4930 * @see {@link Bounds#rectangle} For Rectangle representation 4931 */ 4932 containsPoint(x, y) { 4933 if (this.minX <= x && this.minY <= y && this.maxX >= x && this.maxY >= y) { 4934 return true; 4935 } 4936 return false; 4937 } 4938 /** 4939 * Returns a string representation of the bounds. 4940 * Useful for debugging and logging bounds information. 4941 * @example 4942 * ```ts 4943 * const bounds = new Bounds(0, 0, 100, 100); 4944 * console.log(bounds.toString()); // "[pixi.js:Bounds minX=0 minY=0 maxX=100 maxY=100 width=100 height=100]" 4945 * ``` 4946 * @returns A string describing the bounds 4947 * @see {@link Bounds#copyFrom} For copying bounds 4948 * @see {@link Bounds#clone} For creating a new instance 4949 */ 4950 toString() { 4951 return `[pixi.js:Bounds minX=${this.minX} minY=${this.minY} maxX=${this.maxX} maxY=${this.maxY} width=${this.width} height=${this.height}]`; 4952 } 4953 /** 4954 * Copies the bounds from another bounds object. 4955 * Useful for reusing bounds objects and avoiding allocations. 4956 * @example 4957 * ```ts 4958 * const sourceBounds = new Bounds(0, 0, 100, 100); 4959 * // Copy bounds 4960 * const targetBounds = new Bounds(); 4961 * targetBounds.copyFrom(sourceBounds); 4962 * ``` 4963 * @param bounds - The bounds to copy from 4964 * @returns This bounds object for chaining 4965 * @see {@link Bounds#clone} For creating new instances 4966 */ 4967 copyFrom(bounds) { 4968 this.minX = bounds.minX; 4969 this.minY = bounds.minY; 4970 this.maxX = bounds.maxX; 4971 this.maxY = bounds.maxY; 4972 return this; 4973 } 4974} 4975
vendor: 12,727 bytes, lines 4976-5099
4976var r={grad:.9,turn:360,rad:360/(2*Math.PI)},t=function(r){return "string"==typeof r?r.length>0:"number"==typeof r},n=function(r,t,n){return void 0===t&&(t=0),void 0===n&&(n=Math.pow(10,t)),Math.round(n*r)/n+0},e=function(r,t,n){return void 0===t&&(t=0),void 0===n&&(n=1),r>n?n:r>t?r:t},u=function(r){return (r=isFinite(r)?r%360:0)>0?r:r+360},a=function(r){return {r:e(r.r,0,255),g:e(r.g,0,255),b:e(r.b,0,255),a:e(r.a)}},o=function(r){return {r:n(r.r),g:n(r.g),b:n(r.b),a:n(r.a,3)}},i=/^#([0-9a-f]{3,8})$/i,s=function(r){var t=r.toString(16);return t.length<2?"0"+t:t},h=function(r){var t=r.r,n=r.g,e=r.b,u=r.a,a=Math.max(t,n,e),o=a-Math.min(t,n,e),i=o?a===t?(n-e)/o:a===n?2+(e-t)/o:4+(t-n)/o:0;return {h:60*(i<0?i+6:i),s:a?o/a*100:0,v:a/255*100,a:u}},b=function(r){var t=r.h,n=r.s,e=r.v,u=r.a;t=t/360*6,n/=100,e/=100;var a=Math.floor(t),o=e*(1-n),i=e*(1-(t-a)*n),s=e*(1-(1-t+a)*n),h=a%6;return {r:255*[e,i,o,o,s,e][h],g:255*[s,e,e,i,o,o][h],b:255*[o,o,s,e,e,i][h],a:u}},g=function(r){return {h:u(r.h),s:e(r.s,0,100),l:e(r.l,0,100),a:e(r.a)}},d=function(r){return {h:n(r.h),s:n(r.s),l:n(r.l),a:n(r.a,3)}},f=function(r){return b((n=(t=r).s,{h:t.h,s:(n*=((e=t.l)<50?e:100-e)/100)>0?2*n/(e+n)*100:0,v:e+n,a:t.a}));var t,n,e;},c=function(r){return {h:(t=h(r)).h,s:(u=(200-(n=t.s))*(e=t.v)/100)>0&&u<200?n*e/100/(u<=100?u:200-u)*100:0,l:u/2,a:t.a};var t,n,e,u;},l=/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s*,\s*([+-]?\d*\.?\d+)%\s*,\s*([+-]?\d*\.?\d+)%\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,p=/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s+([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)%\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,v=/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,m=/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,y={string:[[function(r){var t=i.exec(r);return t?(r=t[1]).length<=4?{r:parseInt(r[0]+r[0],16),g:parseInt(r[1]+r[1],16),b:parseInt(r[2]+r[2],16),a:4===r.length?n(parseInt(r[3]+r[3],16)/255,2):1}:6===r.length||8===r.length?{r:parseInt(r.substr(0,2),16),g:parseInt(r.substr(2,2),16),b:parseInt(r.substr(4,2),16),a:8===r.length?n(parseInt(r.substr(6,2),16)/255,2):1}:null:null},"hex"],[function(r){var t=v.exec(r)||m.exec(r);return t?t[2]!==t[4]||t[4]!==t[6]?null:a({r:Number(t[1])/(t[2]?100/255:1),g:Number(t[3])/(t[4]?100/255:1),b:Number(t[5])/(t[6]?100/255:1),a:void 0===t[7]?1:Number(t[7])/(t[8]?100:1)}):null},"rgb"],[function(t){var n=l.exec(t)||p.exec(t);if(!n)return null;var e,u,a=g({h:(e=n[1],u=n[2],void 0===u&&(u="deg"),Number(e)*(r[u]||1)),s:Number(n[3]),l:Number(n[4]),a:void 0===n[5]?1:Number(n[5])/(n[6]?100:1)});return f(a)},"hsl"]],object:[[function(r){var n=r.r,e=r.g,u=r.b,o=r.a,i=void 0===o?1:o;return t(n)&&t(e)&&t(u)?a({r:Number(n),g:Number(e),b:Number(u),a:Number(i)}):null},"rgb"],[function(r){var n=r.h,e=r.s,u=r.l,a=r.a,o=void 0===a?1:a;if(!t(n)||!t(e)||!t(u))return null;var i=g({h:Number(n),s:Number(e),l:Number(u),a:Number(o)});return f(i)},"hsl"],[function(r){var n=r.h,a=r.s,o=r.v,i=r.a,s=void 0===i?1:i;if(!t(n)||!t(a)||!t(o))return null;var h=function(r){return {h:u(r.h),s:e(r.s,0,100),v:e(r.v,0,100),a:e(r.a)}}({h:Number(n),s:Number(a),v:Number(o),a:Number(s)});return b(h)},"hsv"]]},N=function(r,t){for(var n=0;n<t.length;n++){var e=t[n][0](r);if(e)return [e,t[n][1]]}return [null,void 0]},x=function(r){return "string"==typeof r?N(r.trim(),y.string):"object"==typeof r&&null!==r?N(r,y.object):[null,void 0]},M=function(r,t){var n=c(r);return {h:n.h,s:e(n.s+100*t,0,100),l:n.l,a:n.a}},H=function(r){return (299*r.r+587*r.g+114*r.b)/1e3/255},$=function(r,t){var n=c(r);return {h:n.h,s:n.s,l:e(n.l+100*t,0,100),a:n.a}},j=function(){function r(r){this.parsed=x(r)[0],this.rgba=this.parsed||{r:0,g:0,b:0,a:1};}return r.prototype.isValid=function(){return null!==this.parsed},r.prototype.brightness=function(){return n(H(this.rgba),2)},r.prototype.isDark=function(){return H(this.rgba)<.5},r.prototype.isLight=function(){return H(this.rgba)>=.5},r.prototype.toHex=function(){return r=o(this.rgba),t=r.r,e=r.g,u=r.b,i=(a=r.a)<1?s(n(255*a)):"","#"+s(t)+s(e)+s(u)+i;var r,t,e,u,a,i;},r.prototype.toRgb=function(){return o(this.rgba)},r.prototype.toRgbString=function(){return r=o(this.rgba),t=r.r,n=r.g,e=r.b,(u=r.a)<1?"rgba("+t+", "+n+", "+e+", "+u+")":"rgb("+t+", "+n+", "+e+")";var r,t,n,e,u;},r.prototype.toHsl=function(){return d(c(this.rgba))},r.prototype.toHslString=function(){return r=d(c(this.rgba)),t=r.h,n=r.s,e=r.l,(u=r.a)<1?"hsla("+t+", "+n+"%, "+e+"%, "+u+")":"hsl("+t+", "+n+"%, "+e+"%)";var r,t,n,e,u;},r.prototype.toHsv=function(){return r=h(this.rgba),{h:n(r.h),s:n(r.s),v:n(r.v),a:n(r.a,3)};var r;},r.prototype.invert=function(){return w({r:255-(r=this.rgba).r,g:255-r.g,b:255-r.b,a:r.a});var r;},r.prototype.saturate=function(r){return void 0===r&&(r=.1),w(M(this.rgba,r))},r.prototype.desaturate=function(r){return void 0===r&&(r=.1),w(M(this.rgba,-r))},r.prototype.grayscale=function(){return w(M(this.rgba,-1))},r.prototype.lighten=function(r){return void 0===r&&(r=.1),w($(this.rgba,r))},r.prototype.darken=function(r){return void 0===r&&(r=.1),w($(this.rgba,-r))},r.prototype.rotate=function(r){return void 0===r&&(r=15),this.hue(this.hue()+r)},r.prototype.alpha=function(r){return "number"==typeof r?w({r:(t=this.rgba).r,g:t.g,b:t.b,a:r}):n(this.rgba.a,3);var t;},r.prototype.hue=function(r){var t=c(this.rgba);return "number"==typeof r?w({h:r,s:t.s,l:t.l,a:t.a}):n(t.h)},r.prototype.isEqual=function(r){return this.toHex()===w(r).toHex()},r}(),w=function(r){return r instanceof j?r:new j(r)},S=[],k=function(r){r.forEach(function(r){S.indexOf(r)<0&&(r(j,y),S.push(r));});}; 4977 4978function namesPlugin(e,f){var a={white:"#ffffff",bisque:"#ffe4c4",blue:"#0000ff",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",antiquewhite:"#faebd7",aqua:"#00ffff",azure:"#f0ffff",whitesmoke:"#f5f5f5",papayawhip:"#ffefd5",plum:"#dda0dd",blanchedalmond:"#ffebcd",black:"#000000",gold:"#ffd700",goldenrod:"#daa520",gainsboro:"#dcdcdc",cornsilk:"#fff8dc",cornflowerblue:"#6495ed",burlywood:"#deb887",aquamarine:"#7fffd4",beige:"#f5f5dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkkhaki:"#bdb76b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",peachpuff:"#ffdab9",darkmagenta:"#8b008b",darkred:"#8b0000",darkorchid:"#9932cc",darkorange:"#ff8c00",darkslateblue:"#483d8b",gray:"#808080",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",deeppink:"#ff1493",deepskyblue:"#00bfff",wheat:"#f5deb3",firebrick:"#b22222",floralwhite:"#fffaf0",ghostwhite:"#f8f8ff",darkviolet:"#9400d3",magenta:"#ff00ff",green:"#008000",dodgerblue:"#1e90ff",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",blueviolet:"#8a2be2",forestgreen:"#228b22",lawngreen:"#7cfc00",indianred:"#cd5c5c",indigo:"#4b0082",fuchsia:"#ff00ff",brown:"#a52a2a",maroon:"#800000",mediumblue:"#0000cd",lightcoral:"#f08080",darkturquoise:"#00ced1",lightcyan:"#e0ffff",ivory:"#fffff0",lightyellow:"#ffffe0",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",linen:"#faf0e6",mediumaquamarine:"#66cdaa",lemonchiffon:"#fffacd",lime:"#00ff00",khaki:"#f0e68c",mediumseagreen:"#3cb371",limegreen:"#32cd32",mediumspringgreen:"#00fa9a",lightskyblue:"#87cefa",lightblue:"#add8e6",midnightblue:"#191970",lightpink:"#ffb6c1",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",mintcream:"#f5fffa",lightslategray:"#778899",lightslategrey:"#778899",navajowhite:"#ffdead",navy:"#000080",mediumvioletred:"#c71585",powderblue:"#b0e0e6",palegoldenrod:"#eee8aa",oldlace:"#fdf5e6",paleturquoise:"#afeeee",mediumturquoise:"#48d1cc",mediumorchid:"#ba55d3",rebeccapurple:"#663399",lightsteelblue:"#b0c4de",mediumslateblue:"#7b68ee",thistle:"#d8bfd8",tan:"#d2b48c",orchid:"#da70d6",mediumpurple:"#9370db",purple:"#800080",pink:"#ffc0cb",skyblue:"#87ceeb",springgreen:"#00ff7f",palegreen:"#98fb98",red:"#ff0000",yellow:"#ffff00",slateblue:"#6a5acd",lavenderblush:"#fff0f5",peru:"#cd853f",palevioletred:"#db7093",violet:"#ee82ee",teal:"#008080",slategray:"#708090",slategrey:"#708090",aliceblue:"#f0f8ff",darkseagreen:"#8fbc8f",darkolivegreen:"#556b2f",greenyellow:"#adff2f",seagreen:"#2e8b57",seashell:"#fff5ee",tomato:"#ff6347",silver:"#c0c0c0",sienna:"#a0522d",lavender:"#e6e6fa",lightgreen:"#90ee90",orange:"#ffa500",orangered:"#ff4500",steelblue:"#4682b4",royalblue:"#4169e1",turquoise:"#40e0d0",yellowgreen:"#9acd32",salmon:"#fa8072",saddlebrown:"#8b4513",sandybrown:"#f4a460",rosybrown:"#bc8f8f",darksalmon:"#e9967a",lightgoldenrodyellow:"#fafad2",snow:"#fffafa",lightgrey:"#d3d3d3",lightgray:"#d3d3d3",dimgray:"#696969",dimgrey:"#696969",olivedrab:"#6b8e23",olive:"#808000"},r={};for(var d in a)r[a[d]]=d;var l={};e.prototype.toName=function(f){if(!(this.rgba.a||this.rgba.r||this.rgba.g||this.rgba.b))return "transparent";var d,i,n=r[this.toHex()];if(n)return n;if(null==f?void 0:f.closest){var o=this.toRgb(),t=1/0,b="black";if(!l.length)for(var c in a)l[c]=new e(a[c]).toRgb();for(var g in a){var u=(d=o,i=l[g],Math.pow(d.r-i.r,2)+Math.pow(d.g-i.g,2)+Math.pow(d.b-i.b,2));u<t&&(t=u,b=g);}return b}};f.string.push([function(f){var r=f.toLowerCase(),d="transparent"===r?"#0000":a[r];return d?new e(d).toRgb():null},"name"]);} 4979 4980k([namesPlugin]); 4981const _Color = class _Color { 4982 /** 4983 * @param {ColorSource} value - Optional value to use, if not provided, white is used. 4984 */ 4985 constructor(value = 16777215) { 4986 this._value = null; 4987 this._components = new Float32Array(4); 4988 this._components.fill(1); 4989 this._int = 16777215; 4990 this.value = value; 4991 } 4992 /** 4993 * Get the red component of the color, normalized between 0 and 1. 4994 * @example 4995 * ```ts 4996 * const color = new Color('red'); 4997 * console.log(color.red); // 1 4998 * 4999 * const green = new Color('#00ff00'); 5000 * console.log(green.red); // 0 5001 * ``` 5002 */ 5003 get red() { 5004 return this._components[0]; 5005 } 5006 /** 5007 * Get the green component of the color, normalized between 0 and 1. 5008 * @example 5009 * ```ts 5010 * const color = new Color('lime'); 5011 * console.log(color.green); // 1 5012 * 5013 * const red = new Color('#ff0000'); 5014 * console.log(red.green); // 0 5015 * ``` 5016 */ 5017 get green() { 5018 return this._components[1]; 5019 } 5020 /** 5021 * Get the blue component of the color, normalized between 0 and 1. 5022 * @example 5023 * ```ts 5024 * const color = new Color('blue'); 5025 * console.log(color.blue); // 1 5026 * 5027 * const yellow = new Color('#ffff00'); 5028 * console.log(yellow.blue); // 0 5029 * ``` 5030 */ 5031 get blue() { 5032 return this._components[2]; 5033 } 5034 /** 5035 * Get the alpha component of the color, normalized between 0 and 1. 5036 * @example 5037 * ```ts 5038 * const color = new Color('red'); 5039 * console.log(color.alpha); // 1 (fully opaque) 5040 * 5041 * const transparent = new Color('rgba(255, 0, 0, 0.5)'); 5042 * console.log(transparent.alpha); // 0.5 (semi-transparent) 5043 * ``` 5044 */ 5045 get alpha() { 5046 return this._components[3]; 5047 } 5048 /** 5049 * Sets the color value and returns the instance for chaining. 5050 * 5051 * This is a chainable version of setting the `value` property. 5052 * @param value - The color to set. Accepts various formats: 5053 * - Hex strings/numbers (e.g., '#ff0000', 0xff0000) 5054 * - RGB/RGBA values (arrays, objects) 5055 * - CSS color names 5056 * - HSL/HSLA values 5057 * - HSV/HSVA values 5058 * @returns The Color instance for chaining 5059 * @example 5060 * ```ts 5061 * // Basic usage 5062 * const color = new Color(); 5063 * color.setValue('#ff0000') 5064 * .setAlpha(0.5) 5065 * .premultiply(0.8); 5066 * 5067 * // Different formats 5068 * color.setValue(0xff0000); // Hex number 5069 * color.setValue('#ff0000'); // Hex string 5070 * color.setValue([1, 0, 0]); // RGB array 5071 * color.setValue([1, 0, 0, 0.5]); // RGBA array 5072 * color.setValue({ r: 1, g: 0, b: 0 }); // RGB object 5073 * 5074 * // Copy from another color 5075 * const red = new Color('red'); 5076 * color.setValue(red); 5077 * ``` 5078 * @throws {Error} If the color value is invalid or null 5079 * @see {@link Color.value} For the underlying value property 5080 */ 5081 setValue(value) { 5082 this.value = value; 5083 return this; 5084 } 5085 /** 5086 * The current color source. This property allows getting and setting the color value 5087 * while preserving the original format where possible. 5088 * @remarks 5089 * When setting: 5090 * - Setting to a `Color` instance copies its source and components 5091 * - Setting to other valid sources normalizes and stores the value 5092 * - Setting to `null` throws an Error 5093 * - The color remains unchanged if normalization fails 5094 * 5095 * When getting: 5096 * - Returns `null` if color was modified by {@link Color.multiply} or {@link Color.premultiply} 5097 * - Otherwise returns the original color source 5098 * @example 5099 * ```ts
5100 * // Setting different color formats 5101 * const color = new Color(); 5102 * 5103 * color.value = 0xff0000; // Hex number 5104 * color.value = '#ff0000'; // Hex string 5105 * color.value = [1, 0, 0]; // RGB array 5106 * color.value = [1, 0, 0, 0.5]; // RGBA array 5107 * color.value = { r: 1, g: 0, b: 0 }; // RGB object 5108 * 5109 * // Copying from another color 5110 * const red = new Color('red'); 5111 * color.value = red; // Copies red's components 5112 * 5113 * // Getting the value 5114 * console.log(color.value); // Returns original format 5115 * 5116 * // After modifications 5117 * color.multiply([0.5, 0.5, 0.5]); 5118 * console.log(color.value); // Returns null 5119 * ``` 5120 * @throws {Error} When attempting to set `null` 5121 */ 5122 set value(value) { 5123 if (value instanceof _Color) { 5124 this._value = this._cloneSource(value._value); 5125 this._int = value._int; 5126 this._components.set(value._components); 5127 } else if (value === null) { 5128 throw new Error("Cannot set Color#value to null"); 5129 } else if (this._value === null || !this._isSourceEqual(this._value, value)) { 5130 this._value = this._cloneSource(value); 5131 this._normalize(this._value); 5132 } 5133 } 5134 get value() { 5135 return this._value; 5136 } 5137 /** 5138 * Copy a color source internally. 5139 * @param value - Color source 5140 */ 5141 _cloneSource(value) { 5142 if (typeof value === "string" || typeof value === "number" || value instanceof Number || value === null) { 5143 return value; 5144 } else if (Array.isArray(value) || ArrayBuffer.isView(value)) { 5145 return value.slice(0); 5146 } else if (typeof value === "object" && value !== null) { 5147 return { ...value }
vendor: 15,400 bytes, lines 5147-5617
5147; 5148 } 5149 return value; 5150 } 5151 /** 5152 * Equality check for color sources. 5153 * @param value1 - First color source 5154 * @param value2 - Second color source 5155 * @returns `true` if the color sources are equal, `false` otherwise. 5156 */ 5157 _isSourceEqual(value1, value2) { 5158 const type1 = typeof value1; 5159 const type2 = typeof value2; 5160 if (type1 !== type2) { 5161 return false; 5162 } else if (type1 === "number" || type1 === "string" || value1 instanceof Number) { 5163 return value1 === value2; 5164 } else if (Array.isArray(value1) && Array.isArray(value2) || ArrayBuffer.isView(value1) && ArrayBuffer.isView(value2)) { 5165 if (value1.length !== value2.length) { 5166 return false; 5167 } 5168 return value1.every((v, i) => v === value2[i]); 5169 } else if (value1 !== null && value2 !== null) { 5170 const keys1 = Object.keys(value1); 5171 const keys2 = Object.keys(value2); 5172 if (keys1.length !== keys2.length) { 5173 return false; 5174 } 5175 return keys1.every((key) => value1[key] === value2[key]); 5176 } 5177 return value1 === value2; 5178 } 5179 /** 5180 * Convert to a RGBA color object with normalized components (0-1). 5181 * @example 5182 * ```ts 5183 * import { Color } from 'pixi.js'; 5184 * 5185 * // Convert colors to RGBA objects 5186 * new Color('white').toRgba(); // returns { r: 1, g: 1, b: 1, a: 1 } 5187 * new Color('#ff0000').toRgba(); // returns { r: 1, g: 0, b: 0, a: 1 } 5188 * 5189 * // With transparency 5190 * new Color('rgba(255,0,0,0.5)').toRgba(); // returns { r: 1, g: 0, b: 0, a: 0.5 } 5191 * ``` 5192 * @returns An RGBA object with normalized components 5193 */ 5194 toRgba() { 5195 const [r, g, b, a] = this._components; 5196 return { r, g, b, a }; 5197 } 5198 /** 5199 * Convert to a RGB color object with normalized components (0-1). 5200 * 5201 * Alpha component is omitted in the output. 5202 * @example 5203 * ```ts 5204 * import { Color } from 'pixi.js'; 5205 * 5206 * // Convert colors to RGB objects 5207 * new Color('white').toRgb(); // returns { r: 1, g: 1, b: 1 } 5208 * new Color('#ff0000').toRgb(); // returns { r: 1, g: 0, b: 0 } 5209 * 5210 * // Alpha is ignored 5211 * new Color('rgba(255,0,0,0.5)').toRgb(); // returns { r: 1, g: 0, b: 0 } 5212 * ``` 5213 * @returns An RGB object with normalized components 5214 */ 5215 toRgb() { 5216 const [r, g, b] = this._components; 5217 return { r, g, b }; 5218 } 5219 /** 5220 * Convert to a CSS-style rgba string representation. 5221 * 5222 * RGB components are scaled to 0-255 range, alpha remains 0-1. 5223 * @example 5224 * ```ts 5225 * import { Color } from 'pixi.js'; 5226 * 5227 * // Convert colors to RGBA strings 5228 * new Color('white').toRgbaString(); // returns "rgba(255,255,255,1)" 5229 * new Color('#ff0000').toRgbaString(); // returns "rgba(255,0,0,1)" 5230 * 5231 * // With transparency 5232 * new Color([1, 0, 0, 0.5]).toRgbaString(); // returns "rgba(255,0,0,0.5)" 5233 * ``` 5234 * @returns A CSS-compatible rgba string 5235 */ 5236 toRgbaString() { 5237 const [r, g, b] = this.toUint8RgbArray(); 5238 return `rgba(${r},${g},${b},${this.alpha})`; 5239 } 5240 /** 5241 * Convert to an [R, G, B] array of clamped uint8 values (0 to 255). 5242 * @param {number[]|Uint8Array|Uint8ClampedArray} [out] - Optional output array. If not provided, 5243 * a cached array will be used and returned. 5244 * @returns Array containing RGB components as integers between 0-255 5245 * @example 5246 * ```ts 5247 * // Basic usage 5248 * new Color('white').toUint8RgbArray(); // returns [255, 255, 255] 5249 * new Color('#ff0000').toUint8RgbArray(); // returns [255, 0, 0] 5250 * 5251 * // Using custom output array 5252 * const rgb = new Uint8Array(3); 5253 * new Color('blue').toUint8RgbArray(rgb); // rgb is now [0, 0, 255] 5254 * 5255 * // Using different array types 5256 * new Color('red').toUint8RgbArray(new Uint8ClampedArray(3)); // [255, 0, 0] 5257 * new Color('red').toUint8RgbArray([]); // [255, 0, 0] 5258 * ``` 5259 * @remarks 5260 * - Output values are always clamped between 0-255 5261 * - Alpha component is not included in output 5262 * - Reuses internal cache array if no output array provided 5263 */ 5264 toUint8RgbArray(out) { 5265 const [r, g, b] = this._components; 5266 if (!this._arrayRgb) { 5267 this._arrayRgb = []; 5268 } 5269 out || (out = this._arrayRgb); 5270 out[0] = Math.round(r * 255); 5271 out[1] = Math.round(g * 255); 5272 out[2] = Math.round(b * 255); 5273 return out; 5274 } 5275 /** 5276 * Convert to an [R, G, B, A] array of normalized floats (numbers from 0.0 to 1.0). 5277 * @param {number[]|Float32Array} [out] - Optional output array. If not provided, 5278 * a cached array will be used and returned. 5279 * @returns Array containing RGBA components as floats between 0-1 5280 * @example 5281 * ```ts 5282 * // Basic usage 5283 * new Color('white').toArray(); // returns [1, 1, 1, 1] 5284 * new Color('red').toArray(); // returns [1, 0, 0, 1] 5285 * 5286 * // With alpha 5287 * new Color('rgba(255,0,0,0.5)').toArray(); // returns [1, 0, 0, 0.5] 5288 * 5289 * // Using custom output array 5290 * const rgba = new Float32Array(4); 5291 * new Color('blue').toArray(rgba); // rgba is now [0, 0, 1, 1] 5292 * ``` 5293 * @remarks 5294 * - Output values are normalized between 0-1 5295 * - Includes alpha component as the fourth value 5296 * - Reuses internal cache array if no output array provided 5297 */ 5298 toArray(out) { 5299 if (!this._arrayRgba) { 5300 this._arrayRgba = []; 5301 } 5302 out || (out = this._arrayRgba); 5303 const [r, g, b, a] = this._components; 5304 out[0] = r; 5305 out[1] = g; 5306 out[2] = b; 5307 out[3] = a; 5308 return out; 5309 } 5310 /** 5311 * Convert to an [R, G, B] array of normalized floats (numbers from 0.0 to 1.0). 5312 * @param {number[]|Float32Array} [out] - Optional output array. If not provided, 5313 * a cached array will be used and returned. 5314 * @returns Array containing RGB components as floats between 0-1 5315 * @example 5316 * ```ts 5317 * // Basic usage 5318 * new Color('white').toRgbArray(); // returns [1, 1, 1] 5319 * new Color('red').toRgbArray(); // returns [1, 0, 0] 5320 * 5321 * // Using custom output array 5322 * const rgb = new Float32Array(3); 5323 * new Color('blue').toRgbArray(rgb); // rgb is now [0, 0, 1] 5324 * ``` 5325 * @remarks 5326 * - Output values are normalized between 0-1 5327 * - Alpha component is omitted from output 5328 * - Reuses internal cache array if no output array provided 5329 */ 5330 toRgbArray(out) { 5331 if (!this._arrayRgb) { 5332 this._arrayRgb = []; 5333 } 5334 out || (out = this._arrayRgb); 5335 const [r, g, b] = this._components; 5336 out[0] = r; 5337 out[1] = g; 5338 out[2] = b; 5339 return out; 5340 } 5341 /** 5342 * Convert to a hexadecimal number. 5343 * @returns The color as a 24-bit RGB integer 5344 * @example 5345 * ```ts 5346 * // Basic usage 5347 * new Color('white').toNumber(); // returns 0xffffff 5348 * new Color('red').toNumber(); // returns 0xff0000 5349 * 5350 * // Store as hex 5351 * const color = new Color('blue'); 5352 * const hex = color.toNumber(); // 0x0000ff 5353 * ``` 5354 */ 5355 toNumber() { 5356 return this._int; 5357 } 5358 /** 5359 * Convert to a BGR number. 5360 * 5361 * Useful for platforms that expect colors in BGR format. 5362 * @returns The color as a 24-bit BGR integer 5363 * @example 5364 * ```ts 5365 * // Convert RGB to BGR 5366 * new Color(0xffcc99).toBgrNumber(); // returns 0x99ccff 5367 * 5368 * // Common use case: platform-specific color format 5369 * const color = new Color('orange'); 5370 * const bgrColor = color.toBgrNumber(); // Color with swapped R/B channels 5371 * ``` 5372 * @remarks 5373 * This swaps the red and blue channels compared to the normal RGB format: 5374 * - RGB 0xRRGGBB becomes BGR 0xBBGGRR 5375 */ 5376 toBgrNumber() { 5377 const [r, g, b] = this.toUint8RgbArray(); 5378 return (b << 16) + (g << 8) + r; 5379 } 5380 /** 5381 * Convert to a hexadecimal number in little endian format (e.g., BBGGRR). 5382 * 5383 * Useful for platforms that expect colors in little endian byte order. 5384 * @example 5385 * ```ts 5386 * import { Color } from 'pixi.js'; 5387 * 5388 * // Convert RGB color to little endian format 5389 * new Color(0xffcc99).toLittleEndianNumber(); // returns 0x99ccff 5390 * 5391 * // Common use cases: 5392 * const color = new Color('orange'); 5393 * const leColor = color.toLittleEndianNumber(); // Swaps byte order for LE systems 5394 * 5395 * // Multiple conversions 5396 * const colors = { 5397 * normal: 0xffcc99, 5398 * littleEndian: new Color(0xffcc99).toLittleEndianNumber(), // 0x99ccff 5399 * backToNormal: new Color(0x99ccff).toLittleEndianNumber() // 0xffcc99 5400 * }; 5401 * ``` 5402 * @remarks 5403 * - Swaps R and B channels in the color value 5404 * - RGB 0xRRGGBB becomes 0xBBGGRR 5405 * - Useful for systems that use little endian byte order 5406 * - Can be used to convert back and forth between formats 5407 * @returns The color as a number in little endian format (BBGGRR) 5408 * @see {@link Color.toBgrNumber} For BGR format without byte swapping 5409 */ 5410 toLittleEndianNumber() { 5411 const value = this._int; 5412 return (value >> 16) + (value & 65280) + ((value & 255) << 16); 5413 } 5414 /** 5415 * Multiply with another color. 5416 * 5417 * This action is destructive and modifies the original color. 5418 * @param {ColorSource} value - The color to multiply by. Accepts any valid color format: 5419 * - Hex strings/numbers (e.g., '#ff0000', 0xff0000) 5420 * - RGB/RGBA arrays ([1, 0, 0], [1, 0, 0, 1]) 5421 * - Color objects ({ r: 1, g: 0, b: 0 }) 5422 * - CSS color names ('red', 'blue') 5423 * @returns this - The Color instance for chaining 5424 * @example 5425 * ```ts 5426 * // Basic multiplication 5427 * const color = new Color('#ff0000'); 5428 * color.multiply(0x808080); // 50% darker red 5429 * 5430 * // With transparency 5431 * color.multiply([1, 1, 1, 0.5]); // 50% transparent 5432 * 5433 * // Chain operations 5434 * color 5435 * .multiply('#808080') 5436 * .multiply({ r: 1, g: 1, b: 1, a: 0.5 }); 5437 * ``` 5438 * @remarks 5439 * - Multiplies each RGB component and alpha separately 5440 * - Values are clamped between 0-1 5441 * - Original color format is lost (value becomes null) 5442 * - Operation cannot be undone 5443 */ 5444 multiply(value) { 5445 const [r, g, b, a] = _Color._temp.setValue(value)._components; 5446 this._components[0] *= r; 5447 this._components[1] *= g; 5448 this._components[2] *= b; 5449 this._components[3] *= a; 5450 this._refreshInt(); 5451 this._value = null; 5452 return this; 5453 } 5454 /** 5455 * Converts color to a premultiplied alpha format. 5456 * 5457 * This action is destructive and modifies the original color. 5458 * @param alpha - The alpha value to multiply by (0-1) 5459 * @param {boolean} [applyToRGB=true] - Whether to premultiply RGB channels 5460 * @returns {Color} The Color instance for chaining 5461 * @example 5462 * ```ts 5463 * // Basic premultiplication 5464 * const color = new Color('red'); 5465 * color.premultiply(0.5); // 50% transparent red with premultiplied RGB 5466 * 5467 * // Alpha only (RGB unchanged) 5468 * color.premultiply(0.5, false); // 50% transparent, original RGB 5469 * 5470 * // Chain with other operations 5471 * color 5472 * .multiply(0x808080) 5473 * .premultiply(0.5) 5474 * .toNumber(); 5475 * ``` 5476 * @remarks 5477 * - RGB channels are multiplied by alpha when applyToRGB is true 5478 * - Alpha is always set to the provided value 5479 * - Values are clamped between 0-1 5480 * - Original color format is lost (value becomes null) 5481 * - Operation cannot be undone 5482 */ 5483 premultiply(alpha, applyToRGB = true) { 5484 if (applyToRGB) { 5485 this._components[0] *= alpha; 5486 this._components[1] *= alpha; 5487 this._components[2] *= alpha; 5488 } 5489 this._components[3] = alpha; 5490 this._refreshInt(); 5491 this._value = null; 5492 return this; 5493 } 5494 /** 5495 * Returns the color as a 32-bit premultiplied alpha integer. 5496 * 5497 * Format: 0xAARRGGBB 5498 * @param {number} alpha - The alpha value to multiply by (0-1) 5499 * @param {boolean} [applyToRGB=true] - Whether to premultiply RGB channels 5500 * @returns {number} The premultiplied color as a 32-bit integer 5501 * @example 5502 * ```ts 5503 * // Convert to premultiplied format 5504 * const color = new Color('red'); 5505 * 5506 * // Full opacity (0xFFRRGGBB) 5507 * color.toPremultiplied(1.0); // 0xFFFF0000 5508 * 5509 * // 50% transparency with premultiplied RGB 5510 * color.toPremultiplied(0.5); // 0x7F7F0000 5511 * 5512 * // 50% transparency without RGB premultiplication 5513 * color.toPremultiplied(0.5, false); // 0x7FFF0000 5514 * ``` 5515 * @remarks 5516 * - Returns full opacity (0xFF000000) when alpha is 1.0 5517 * - Returns 0 when alpha is 0.0 and applyToRGB is true 5518 * - RGB values are rounded during premultiplication 5519 */ 5520 toPremultiplied(alpha, applyToRGB = true) { 5521 if (alpha === 1) { 5522 return (255 << 24) + this._int; 5523 } 5524 if (alpha === 0) { 5525 return applyToRGB ? 0 : this._int; 5526 } 5527 let r = this._int >> 16 & 255; 5528 let g = this._int >> 8 & 255; 5529 let b = this._int & 255; 5530 if (applyToRGB) { 5531 r = r * alpha + 0.5 | 0; 5532 g = g * alpha + 0.5 | 0; 5533 b = b * alpha + 0.5 | 0; 5534 } 5535 return (alpha * 255 << 24) + (r << 16) + (g << 8) + b; 5536 } 5537 /** 5538 * Convert to a hexadecimal string (6 characters). 5539 * @returns A CSS-compatible hex color string (e.g., "#ff0000") 5540 * @example 5541 * ```ts 5542 * import { Color } from 'pixi.js'; 5543 * 5544 * // Basic colors 5545 * new Color('red').toHex(); // returns "#ff0000" 5546 * new Color('white').toHex(); // returns "#ffffff" 5547 * new Color('black').toHex(); // returns "#000000" 5548 * 5549 * // From different formats 5550 * new Color(0xff0000).toHex(); // returns "#ff0000" 5551 * new Color([1, 0, 0]).toHex(); // returns "#ff0000" 5552 * new Color({ r: 1, g: 0, b: 0 }).toHex(); // returns "#ff0000" 5553 * ``` 5554 * @remarks 5555 * - Always returns a 6-character hex string 5556 * - Includes leading "#" character 5557 * - Alpha channel is ignored 5558 * - Values are rounded to nearest hex value 5559 */ 5560 toHex() { 5561 const hexString = this._int.toString(16); 5562 return `#${"000000".substring(0, 6 - hexString.length) + hexString}`; 5563 } 5564 /** 5565 * Convert to a hexadecimal string with alpha (8 characters). 5566 * @returns A CSS-compatible hex color string with alpha (e.g., "#ff0000ff") 5567 * @example 5568 * ```ts 5569 * import { Color } from 'pixi.js'; 5570 * 5571 * // Fully opaque colors 5572 * new Color('red').toHexa(); // returns "#ff0000ff" 5573 * new Color('white').toHexa(); // returns "#ffffffff" 5574 * 5575 * // With transparency 5576 * new Color('rgba(255, 0, 0, 0.5)').toHexa(); // returns "#ff00007f" 5577 * new Color([1, 0, 0, 0]).toHexa(); // returns "#ff000000" 5578 * ``` 5579 * @remarks 5580 * - Returns an 8-character hex string 5581 * - Includes leading "#" character 5582 * - Alpha is encoded in last two characters 5583 * - Values are rounded to nearest hex value 5584 */ 5585 toHexa() { 5586 const alphaValue = Math.round(this._components[3] * 255); 5587 const alphaString = alphaValue.toString(16); 5588 return this.toHex() + "00".substring(0, 2 - alphaString.length) + alphaString; 5589 } 5590 /** 5591 * Set alpha (transparency) value while preserving color components. 5592 * 5593 * Provides a chainable interface for setting alpha. 5594 * @param alpha - Alpha value between 0 (fully transparent) and 1 (fully opaque) 5595 * @returns The Color instance for chaining 5596 * @example 5597 * ```ts 5598 * // Basic alpha setting 5599 * const color = new Color('red'); 5600 * color.setAlpha(0.5); // 50% transparent red 5601 * 5602 * // Chain with other operations 5603 * color 5604 * .setValue('#ff0000') 5605 * .setAlpha(0.8) // 80% opaque 5606 * .premultiply(0.5); // Further modify alpha 5607 * 5608 * // Reset to fully opaque 5609 * color.setAlpha(1); 5610 * ``` 5611 * @remarks 5612 * - Alpha value is clamped between 0-1 5613 * - Can be chained with other color operations 5614 */ 5615 setAlpha(alpha) { 5616 this._components[3] = this._clamp(alpha); 5617
5617return this; 5618 } 5619 /** 5620 * Normalize the input value into rgba
vendor: 7,895 bytes, lines 5621-5836
5621 * @param value - Input value 5622 */ 5623 _normalize(value) { 5624 let r; 5625 let g; 5626 let b; 5627 let a; 5628 if ((typeof value === "number" || value instanceof Number) && value >= 0 && value <= 16777215) { 5629 const int = value; 5630 r = (int >> 16 & 255) / 255; 5631 g = (int >> 8 & 255) / 255; 5632 b = (int & 255) / 255; 5633 a = 1; 5634 } else if ((Array.isArray(value) || value instanceof Float32Array) && value.length >= 3 && value.length <= 4) { 5635 value = this._clamp(value); 5636 [r, g, b, a = 1] = value; 5637 } else if ((value instanceof Uint8Array || value instanceof Uint8ClampedArray) && value.length >= 3 && value.length <= 4) { 5638 value = this._clamp(value, 0, 255); 5639 [r, g, b, a = 255] = value; 5640 r /= 255; 5641 g /= 255; 5642 b /= 255; 5643 a /= 255; 5644 } else if (typeof value === "string" || typeof value === "object") { 5645 if (typeof value === "string") { 5646 const match = _Color.HEX_PATTERN.exec(value); 5647 if (match) { 5648 value = `#${match[2]}`; 5649 } 5650 } 5651 const color = w(value); 5652 if (color.isValid()) { 5653 ({ r, g, b, a } = color.rgba); 5654 r /= 255; 5655 g /= 255; 5656 b /= 255; 5657 } 5658 } 5659 if (r !== void 0) { 5660 this._components[0] = r; 5661 this._components[1] = g; 5662 this._components[2] = b; 5663 this._components[3] = a; 5664 this._refreshInt(); 5665 } else { 5666 throw new Error(`Unable to convert color ${value}`); 5667 } 5668 } 5669 /** Refresh the internal color rgb number */ 5670 _refreshInt() { 5671 this._clamp(this._components); 5672 const [r, g, b] = this._components; 5673 this._int = (r * 255 << 16) + (g * 255 << 8) + (b * 255 | 0); 5674 } 5675 /** 5676 * Clamps values to a range. Will override original values 5677 * @param value - Value(s) to clamp 5678 * @param min - Minimum value 5679 * @param max - Maximum value 5680 */ 5681 _clamp(value, min = 0, max = 1) { 5682 if (typeof value === "number") { 5683 return Math.min(Math.max(value, min), max); 5684 } 5685 value.forEach((v, i) => { 5686 value[i] = Math.min(Math.max(v, min), max); 5687 }); 5688 return value; 5689 } 5690 /** 5691 * Check if a value can be interpreted as a valid color format. 5692 * Supports all color formats that can be used with the Color class. 5693 * @param value - Value to check 5694 * @returns True if the value can be used as a color 5695 * @example 5696 * ```ts 5697 * import { Color } from 'pixi.js'; 5698 * 5699 * // CSS colors and hex values 5700 * Color.isColorLike('red'); // true 5701 * Color.isColorLike('#ff0000'); // true 5702 * Color.isColorLike(0xff0000); // true 5703 * 5704 * // Arrays (RGB/RGBA) 5705 * Color.isColorLike([1, 0, 0]); // true 5706 * Color.isColorLike([1, 0, 0, 0.5]); // true 5707 * 5708 * // TypedArrays 5709 * Color.isColorLike(new Float32Array([1, 0, 0])); // true 5710 * Color.isColorLike(new Uint8Array([255, 0, 0])); // true 5711 * Color.isColorLike(new Uint8ClampedArray([255, 0, 0])); // true 5712 * 5713 * // Object formats 5714 * Color.isColorLike({ r: 1, g: 0, b: 0 }); // true (RGB) 5715 * Color.isColorLike({ r: 1, g: 0, b: 0, a: 0.5 }); // true (RGBA) 5716 * Color.isColorLike({ h: 0, s: 100, l: 50 }); // true (HSL) 5717 * Color.isColorLike({ h: 0, s: 100, l: 50, a: 0.5 }); // true (HSLA) 5718 * Color.isColorLike({ h: 0, s: 100, v: 100 }); // true (HSV) 5719 * Color.isColorLike({ h: 0, s: 100, v: 100, a: 0.5 });// true (HSVA) 5720 * 5721 * // Color instances 5722 * Color.isColorLike(new Color('red')); // true 5723 * 5724 * // Invalid values 5725 * Color.isColorLike(null); // false 5726 * Color.isColorLike(undefined); // false 5727 * Color.isColorLike({}); // false 5728 * Color.isColorLike([]); // false 5729 * Color.isColorLike('not-a-color'); // false 5730 * ``` 5731 * @remarks 5732 * Checks for the following formats: 5733 * - Numbers (0x000000 to 0xffffff) 5734 * - CSS color strings 5735 * - RGB/RGBA arrays and objects 5736 * - HSL/HSLA objects 5737 * - HSV/HSVA objects 5738 * - TypedArrays (Float32Array, Uint8Array, Uint8ClampedArray) 5739 * - Color instances 5740 * @see {@link ColorSource} For supported color format types 5741 * @see {@link Color.setValue} For setting color values 5742 * @category utility 5743 */ 5744 static isColorLike(value) { 5745 return typeof value === "number" || typeof value === "string" || value instanceof Number || value instanceof _Color || Array.isArray(value) || value instanceof Uint8Array || value instanceof Uint8ClampedArray || value instanceof Float32Array || value.r !== void 0 && value.g !== void 0 && value.b !== void 0 || value.r !== void 0 && value.g !== void 0 && value.b !== void 0 && value.a !== void 0 || value.h !== void 0 && value.s !== void 0 && value.l !== void 0 || value.h !== void 0 && value.s !== void 0 && value.l !== void 0 && value.a !== void 0 || value.h !== void 0 && value.s !== void 0 && value.v !== void 0 || value.h !== void 0 && value.s !== void 0 && value.v !== void 0 && value.a !== void 0; 5746 } 5747}; 5748/** 5749 * Static shared Color instance used for utility operations. This is a singleton color object 5750 * that can be reused to avoid creating unnecessary Color instances. 5751 * > [!IMPORTANT] You should be careful when using this shared instance, as it is mutable and can be 5752 * > changed by any code that uses it. 5753 * > 5754 * > It is best used for one-off color operations or temporary transformations. 5755 * > For persistent colors, create your own Color instance instead. 5756 * @example 5757 * ```ts 5758 * import { Color } from 'pixi.js'; 5759 * 5760 * // Use shared instance for one-off color operations 5761 * Color.shared.setValue(0xff0000); 5762 * const redHex = Color.shared.toHex(); // "#ff0000" 5763 * const redRgb = Color.shared.toRgbArray(); // [1, 0, 0] 5764 * 5765 * // Temporary color transformations 5766 * const colorNumber = Color.shared 5767 * .setValue('#ff0000') // Set to red 5768 * .setAlpha(0.5) // Make semi-transparent 5769 * .premultiply(0.8) // Apply premultiplication 5770 * .toNumber(); // Convert to number 5771 * 5772 * // Chain multiple operations 5773 * const result = Color.shared 5774 * .setValue(someColor) 5775 * .multiply(tintColor) 5776 * .toPremultiplied(alpha); 5777 * ``` 5778 * @remarks 5779 * - This is a shared instance - be careful about multiple code paths using it simultaneously 5780 * - Use for temporary color operations to avoid allocating new Color instances 5781 * - The value is preserved between operations, so reset if needed 5782 * - For persistent colors, create your own Color instance instead 5783 */ 5784_Color.shared = new _Color(); 5785/** 5786 * Temporary Color object for static uses internally. 5787 * As to not conflict with Color.shared. 5788 * @ignore 5789 */ 5790_Color._temp = new _Color(); 5791/** Pattern for hex strings */ 5792// eslint-disable-next-line @typescript-eslint/naming-convention 5793_Color.HEX_PATTERN = /^(#|0x)?(([a-f0-9]{3}){1,2}([a-f0-9]{2})?)$/i; 5794let Color = _Color; 5795 5796const cullingMixin = { 5797 cullArea: null, 5798 cullable: false, 5799 cullableChildren: true 5800}; 5801 5802let warnCount = 0; 5803const maxWarnings = 500; 5804function warn(...args) { 5805 if (warnCount === maxWarnings) 5806 return; 5807 warnCount++; 5808 if (warnCount === maxWarnings) { 5809 console.warn("PixiJS Warning: too many warnings, no more warnings will be reported to the console by PixiJS."); 5810 } else { 5811 console.warn("PixiJS Warning: ", ...args); 5812 } 5813} 5814 5815const GlobalResourceRegistry = { 5816 /** 5817 * Set of registered pools and cleanable objects. 5818 * @private 5819 */ 5820 _registeredResources: /* @__PURE__ */ new Set(), 5821 /** 5822 * Registers a pool or cleanable object for cleanup. 5823 * @param {Cleanable} pool - The pool or object to register. 5824 */ 5825 register(pool) { 5826 this._registeredResources.add(pool); 5827 }, 5828 /** 5829 * Unregisters a pool or cleanable object from cleanup. 5830 * @param {Cleanable} pool - The pool or object to unregister. 5831 */ 5832 unregister(pool) { 5833 this._registeredResources.delete(pool); 5834 }, 5835 /** Clears all registered pools and cleanable objects. This will call clear() on each registered item. */ 5836 release() {
5837 this._registeredResources.forEach((pool) => pool.clear()); 5838 }, 5839 /** 5840 * Gets the number of registered pools and cleanable objects. 5841 * @returns {number} The count of registered items. 5842 */ 5843 get registeredCount() { 5844 return this._registeredResources.size; 5845 }, 5846 /** 5847 * Checks if a specific pool or cleanable object is registered. 5848 * @param {Cleanable} pool - The pool or object to check. 5849 * @returns {boolean} True if the item is registered, false otherwise. 5850 */ 5851 isRegistered(pool) { 5852 return this._registeredResources.has(pool); 5853 }, 5854 /** 5855 * Removes all registrations without clearing the pools. 5856 * Useful if you want to reset the collector without affecting the pools. 5857 */ 5858 reset() { 5859 this._registeredResources.clear(); 5860 } 5861}; 5862 5863class Pool { 5864 /** 5865 * Constructs a new Pool. 5866 * @param ClassType - The constructor of the items in the pool. 5867 * @param {number} [initialSize] - The initial size of the pool. 5868 */ 5869 constructor(ClassType, initialSize) { 5870 this._pool = []; 5871 this._count = 0; 5872 this._index = 0; 5873 this._classType = ClassType; 5874 if (initialSize) { 5875 this.prepopulate(initialSize); 5876 } 5877 } 5878 /** 5879 * Prepopulates the pool with a given number of items. 5880 * @param total - The number of items to add to the pool. 5881 */ 5882 prepopulate(total) { 5883 for (let i = 0; i < total; i++) { 5884 this._pool[this._index++] = new this._classType(); 5885 } 5886 this._count += total; 5887 } 5888 /** 5889 * Gets an item from the pool. Calls the item's `init` method if it exists. 5890 * If there are no items left in the pool, a new one will be created. 5891 * @param {unknown} [data] - Optional data to pass to the item's constructor. 5892 * @returns {T} The item from the pool. 5893 */ 5894 get(data) { 5895 let item; 5896 if (this._index > 0) { 5897 item = this._pool[--this._index]; 5898 } else { 5899 item = new this._classType(); 5900 this._count++; 5901 } 5902 item.init?.(data); 5903 return item; 5904 } 5905 /** 5906 * Returns an item to the pool. Calls the item's `reset` method if it exists. 5907 * @param {T} item - The item to return to the pool. 5908 */ 5909 return(item) { 5910 item.reset?.(); 5911 this._pool[this._index++] = item; 5912 } 5913 /** 5914 * Gets the number of items in the pool. 5915 * @readonly 5916 */ 5917 get totalSize() { 5918 return this._count; 5919 } 5920 /** 5921 * Gets the number of items in the pool that are free to use without needing to create more. 5922 * @readonly 5923 */ 5924 get totalFree() { 5925 return this._index; 5926 } 5927 /** 5928 * Gets the number of items in the pool that are currently in use. 5929 * @readonly 5930 */ 5931 get totalUsed() { 5932 return this._count - this._index; 5933 } 5934 /** clears the pool */ 5935 clear() { 5936 if (this._pool.length > 0 && this._pool[0].destroy) { 5937 for (let i = 0; i < this._index; i++) { 5938 this._pool[i].destroy(); 5939 } 5940 } 5941 this._pool.length = 0; 5942 this._count = 0; 5943 this._index = 0; 5944 } 5945} 5946 5947class PoolGroupClass { 5948 constructor() { 5949 /** 5950 * A map to store the pools by their class type. 5951 * @private 5952 */ 5953 this._poolsByClass = /* @__PURE__ */ new Map(); 5954 } 5955 /** 5956 * Prepopulates a specific pool with a given number of items. 5957 * @template T The type of items in the pool. Must extend PoolItem. 5958 * @param {PoolItemConstructor<T>} Class - The constructor of the items in the pool. 5959 * @param {number} total - The number of items to add to the pool. 5960 */ 5961 prepopulate(Class, total) { 5962 const classPool = this.getPool(Class); 5963 classPool.prepopulate(total); 5964 } 5965 /** 5966 * Gets an item from a specific pool. 5967 * @template T The type of items in the pool. Must extend PoolItem. 5968 * @param {PoolItemConstructor<T>} Class - The constructor of the items in the pool. 5969 * @param {unknown} [data] - Optional data to pass to the item's constructor. 5970 * @returns {T} The item from the pool. 5971 */ 5972 get(Class, data) { 5973 const pool = this.getPool(Class); 5974 return pool.get(data); 5975 } 5976 /** 5977 * Returns an item to its respective pool. 5978 * @param {PoolItem} item - The item to return to the pool. 5979 */ 5980 return(item) { 5981 const pool = this.getPool(item.constructor); 5982 pool.return(item); 5983 } 5984 /** 5985 * Gets a specific pool based on the class type. 5986 * @template T The type of items in the pool. Must extend PoolItem. 5987 * @param {PoolItemConstructor<T>} ClassType - The constructor of the items in the pool. 5988 * @returns {Pool<T>} The pool of the given class type. 5989 */ 5990 getPool(ClassType) { 5991 if (!this._poolsByClass.has(ClassType)) { 5992 this._poolsByClass.set(ClassType, new Pool(ClassType)); 5993 } 5994 return this._poolsByClass.get(ClassType); 5995 } 5996 /** gets the usage stats of each pool in the system */ 5997 stats() { 5998 const stats = {}; 5999 this._poolsByClass.forEach((pool) => { 6000 const name = stats[pool._classType.name] ? pool._classType.name + pool._classType.ID : pool._classType.name; 6001 stats[name] = { 6002 free: pool.totalFree, 6003 used: pool.totalUsed, 6004 size: pool.totalSize 6005 }; 6006 }); 6007 return stats; 6008 } 6009 /** Clears all pools in the group. This will reset all pools and free their resources. */ 6010 clear() { 6011 this._poolsByClass.forEach((pool) => pool.clear()); 6012 this._poolsByClass.clear(); 6013 } 6014} 6015const BigPool = new PoolGroupClass(); 6016GlobalResourceRegistry.register(BigPool); 6017 6018const cacheAsTextureMixin = { 6019 get isCachedAsTexture() { 6020 return !!this.renderGroup?.isCachedAsTexture; 6021 }, 6022 cacheAsTexture(val) { 6023 if (typeof val === "boolean" && val === false) { 6024 this.disableRenderGroup(); 6025 } else { 6026 this.enableRenderGroup(); 6027 this.renderGroup.enableCacheAsTexture(val === true ? {} : val); 6028 } 6029 }, 6030 updateCacheTexture() { 6031 this.renderGroup?.updateCacheTexture(); 6032 }, 6033 get cacheAsBitmap() { 6034 return this.isCachedAsTexture; 6035 }, 6036 set cacheAsBitmap(val) {
6037 deprecation("v8.6.0", "cacheAsBitmap is deprecated, use cacheAsTexture instead."); 6038 this.cacheAsTexture(val); 6039 } 6040}; 6041 6042function removeItems(arr, startIdx, removeCount) { 6043 const length = arr.length; 6044 let i; 6045 if (startIdx >= length || removeCount === 0) { 6046 return; 6047 } 6048 removeCount = startIdx + removeCount > length ? length - startIdx : removeCount; 6049 const len = length - removeCount; 6050 for (i = startIdx; i < len; ++i) { 6051 arr[i] = arr[i + removeCount]; 6052 } 6053 arr.length = len; 6054} 6055 6056const childrenHelperMixin = { 6057 allowChildren: true, 6058 removeChildren(beginIndex = 0, endIndex) { 6059 const end = endIndex ?? this.children.length; 6060 const range = end - beginIndex; 6061 const removed = []; 6062 if (range > 0 && range <= end) { 6063 for (let i = end - 1; i >= beginIndex; i--) { 6064 const child = this.children[i]; 6065 if (!child) 6066 continue; 6067 removed.push(child); 6068 child.parent = null; 6069 } 6070 removeItems(this.children, beginIndex, end); 6071 const renderGroup = this.renderGroup || this.parentRenderGroup; 6072 if (renderGroup) { 6073 renderGroup.removeChildren(removed); 6074 } 6075 for (let i = 0; i < removed.length; ++i) { 6076 const child = removed[i]; 6077 child.parentRenderLayer?.detach(child); 6078 this.emit("childRemoved", child, this, i); 6079 removed[i].emit("removed", this); 6080 } 6081 if (removed.length > 0) { 6082 this._didViewChangeTick++; 6083 } 6084 return removed; 6085 } else if (range === 0 && this.children.length === 0) { 6086 return removed; 6087 } 6088 throw new RangeError("removeChildren: numeric values are outside the acceptable range."); 6089 }, 6090 removeChildAt(index) { 6091 const child = this.getChildAt(index); 6092 return this.removeChild(child); 6093 }, 6094 getChildAt(index) { 6095 if (index < 0 || index >= this.children.length) { 6096 throw new Error(`getChildAt: Index (${index}) does not exist.`); 6097 } 6098 return this.children[index]; 6099 }, 6100 setChildIndex(child, index) { 6101 if (index < 0 || index >= this.children.length) { 6102 throw new Error(`The index ${index} supplied is out of bounds ${this.children.length}`); 6103 } 6104 this.getChildIndex(child); 6105 this.addChildAt(child, index); 6106 }, 6107 getChildIndex(child) { 6108 const index = this.children.indexOf(child); 6109 if (index === -1) { 6110 throw new Error("The supplied Container must be a child of the caller"); 6111 } 6112 return index; 6113 }, 6114 addChildAt(child, index) { 6115 if (!this.allowChildren) { 6116 deprecation(v8_0_0, "addChildAt: Only Containers will be allowed to add children in v8.0.0"); 6117 } 6118 const { children } = this; 6119 if (index < 0 || index > children.length) { 6120 throw new Error(`${child}addChildAt: The index ${index} supplied is out of bounds ${children.length}`); 6121 } 6122 if (child.parent) { 6123 const currentIndex = child.parent.children.indexOf(child); 6124 if (child.parent === this && currentIndex === index) { 6125 return child; 6126 } 6127 if (currentIndex !== -1) { 6128 child.parent.children.splice(currentIndex, 1); 6129 } 6130 } 6131 if (index === children.length) { 6132 children.push(child); 6133 } else { 6134 children.splice(index, 0, child); 6135 } 6136 child.parent = this; 6137 child.didChange = true; 6138 child._updateFlags = 15; 6139 const renderGroup = this.renderGroup || this.parentRenderGroup; 6140 if (renderGroup) { 6141 renderGroup.addChild(child); 6142 } 6143 if (this.sortableChildren) 6144 this.sortDirty = true; 6145 this.emit("childAdded", child, this, index); 6146 child.emit("added", this); 6147 return child; 6148 }, 6149 swapChildren(child, child2) { 6150 if (child === child2) { 6151 return; 6152 } 6153 const index1 = this.getChildIndex(child); 6154 const index2 = this.getChildIndex(child2); 6155 this.children[index1] = child2; 6156 this.children[index2] = child; 6157 const renderGroup = this.renderGroup || this.parentRenderGroup; 6158 if (renderGroup) { 6159 renderGroup.structureDidChange = true; 6160 } 6161 this._didContainerChangeTick++; 6162 }, 6163 removeFromParent() { 6164 this.parent?.removeChild(this); 6165 }, 6166 reparentChild(...child) { 6167 if (child.length === 1) { 6168 return this.reparentChildAt(child[0], this.children.length); 6169 } 6170 child.forEach((c) => this.reparentChildAt(c, this.children.length)); 6171 return child[0]; 6172 }, 6173 reparentChildAt(child, index) { 6174 if (child.parent === this) { 6175 this.setChildIndex(child, index); 6176 return child; 6177 } 6178 const childMat = child.worldTransform.clone(); 6179 child.removeFromParent(); 6180 this.addChildAt(child, index); 6181 const newMatrix = this.worldTransform.clone(); 6182 newMatrix.invert();
6183 childMat.prepend(newMatrix); 6184 child.setFromMatrix(childMat); 6185 return child; 6186 }, 6187 replaceChild(oldChild, newChild) { 6188 oldChild.updateLocalTransform(); 6189 this.addChildAt(newChild, this.getChildIndex(oldChild)); 6190 newChild.setFromMatrix(oldChild.localTransform); 6191 newChild.updateLocalTransform(); 6192 this.removeChild(oldChild); 6193 } 6194}; 6195 6196const collectRenderablesMixin = { 6197 collectRenderables(instructionSet, renderer, currentLayer) { 6198 if (this.parentRenderLayer && this.parentRenderLayer !== currentLayer || this.globalDisplayStatus < 7 || !this.includeInBuild) 6199 return; 6200 if (this.sortableChildren) { 6201 this.sortChildren(); 6202 } 6203 if (this.isSimple) { 6204 this.collectRenderablesSimple(instructionSet, renderer, currentLayer); 6205 } else if (this.renderGroup) { 6206 renderer.renderPipes.renderGroup.addRenderGroup(this.renderGroup, instructionSet); 6207 } else { 6208 this.collectRenderablesWithEffects(instructionSet, renderer, currentLayer); 6209 } 6210 }, 6211 collectRenderablesSimple(instructionSet, renderer, currentLayer) { 6212 const children = this.children; 6213 const length = children.length; 6214 for (let i = 0; i < length; i++) { 6215 children[i].collectRenderables(instructionSet, renderer, currentLayer); 6216 } 6217 }, 6218 collectRenderablesWithEffects(instructionSet, renderer, currentLayer) { 6219 const { renderPipes } = renderer; 6220 for (let i = 0; i < this.effects.length; i++) { 6221 const effect = this.effects[i]; 6222 const pipe = renderPipes[effect.pipe]; 6223 pipe.push(effect, this, instructionSet); 6224 } 6225 this.collectRenderablesSimple(instructionSet, renderer, currentLayer); 6226 for (let i = this.effects.length - 1; i >= 0; i--) { 6227 const effect = this.effects[i]; 6228 const pipe = renderPipes[effect.pipe]; 6229 pipe.pop(effect, this, instructionSet); 6230 } 6231 } 6232}; 6233 6234class FilterEffect { 6235 constructor() { 6236 /** the pipe that knows how to handle this effect */ 6237 this.pipe = "filter"; 6238 /** the priority of this effect */ 6239 this.priority = 1; 6240 } 6241 destroy() { 6242 for (let i = 0; i < this.filters.length; i++) { 6243 this.filters[i].destroy(); 6244 } 6245 this.filters = null; 6246 this.filterArea = null; 6247 } 6248} 6249 6250class MaskEffectManagerClass { 6251 constructor() { 6252 /** @private */ 6253 this._effectClasses = []; 6254 this._tests = []; 6255 this._initialized = false; 6256 } 6257 init() { 6258 if (this._initialized) 6259 return; 6260 this._initialized = true; 6261 this._effectClasses.forEach((test) => { 6262 this.add({ 6263 test: test.test, 6264 maskClass: test 6265 }); 6266 }); 6267 } 6268 add(test) { 6269 this._tests.push(test); 6270 } 6271 getMaskEffect(item) { 6272 if (!this._initialized) 6273 this.init(); 6274 for (let i = 0; i < this._tests.length; i++) { 6275 const test = this._tests[i]; 6276 if (test.test(item)) { 6277 return BigPool.get(test.maskClass, item); 6278 } 6279 } 6280 return item; 6281 } 6282 returnMaskEffect(effect) { 6283 BigPool.return(effect); 6284 } 6285} 6286const MaskEffectManager = new MaskEffectManagerClass(); 6287extensions.handleByList(ExtensionType.MaskEffect, MaskEffectManager._effectClasses); 6288 6289const effectsMixin = { 6290 _maskEffect: null, 6291 _maskOptions: { 6292 inverse: false 6293 }, 6294 _filterEffect: null, 6295 effects: [], 6296 _markStructureAsChanged() { 6297 const renderGroup = this.renderGroup || this.parentRenderGroup; 6298 if (renderGroup) { 6299 renderGroup.structureDidChange = true; 6300 } 6301 }, 6302 addEffect(effect) { 6303 const index = this.effects.indexOf(effect); 6304 if (index !== -1) 6305 return; 6306 this.effects.push(effect); 6307 this.effects.sort((a, b) => a.priority - b.priority); 6308 this._markStructureAsChanged(); 6309 this._updateIsSimple(); 6310 }, 6311 removeEffect(effect) { 6312 const index = this.effects.indexOf(effect); 6313 if (index === -1) 6314 return; 6315 this.effects.splice(index, 1); 6316 this._markStructureAsChanged(); 6317 this._updateIsSimple(); 6318 }, 6319 set mask(value) { 6320 const effect = this._maskEffect; 6321 if (effect?.mask === value) 6322 return; 6323 if (effect) { 6324 this.removeEffect(effect); 6325 MaskEffectManager.returnMaskEffect(effect); 6326 this._maskEffect = null; 6327 } 6328 if (value === null || value === void 0) 6329 return; 6330 this._maskEffect = MaskEffectManager.getMaskEffect(value); 6331 this.addEffect(this._maskEffect); 6332 }, 6333 get mask() { 6334 return this._maskEffect?.mask; 6335 }, 6336 setMask(options) { 6337 this._maskOptions = { 6338 ...this._maskOptions, 6339 ...options 6340 }; 6341 if (options.mask) { 6342 this.mask = options.mask; 6343 } 6344 this._markStructureAsChanged(); 6345 }, 6346 set filters(value) { 6347 if (!Array.isArray(value) && value) 6348 value = [value]; 6349 const effect = this._filterEffect || (this._filterEffect = new FilterEffect()); 6350 value = value; 6351 const hasFilters = value?.length > 0; 6352 const hadFilters = effect.filters?.length > 0; 6353 const didChange = hasFilters !== hadFilters; 6354 value = Array.isArray(value) ? value.slice(0) : value; 6355 effect.filters = Object.freeze(value); 6356 if (didChange) { 6357 if (hasFilters) { 6358 this.addEffect(effect); 6359 } else { 6360 this.removeEffect(effect); 6361 effect.filters = value ?? null; 6362 } 6363 } 6364 }, 6365 get filters() { 6366 return this._filterEffect?.filters; 6367 }, 6368 set filterArea(value) { 6369 this._filterEffect || (this._filterEffect = new FilterEffect()); 6370 this._filterEffect.filterArea = value; 6371 }, 6372 get filterArea() { 6373 return this._filterEffect?.filterArea; 6374 } 6375}; 6376 6377const findMixin = { 6378 label: null, 6379 get name() { 6380 deprecation(v8_0_0, "Container.name property has been removed, use Container.label instead"); 6381 return this.label; 6382 }, 6383 set name(value) { 6384 deprecation(v8_0_0, "Container.name property has been removed, use Container.label instead"); 6385 this.label = value; 6386 }, 6387 getChildByName(name, deep = false) { 6388 return this.getChildByLabel(name, deep); 6389 }, 6390 getChildByLabel(label, deep = false) { 6391 const children = this.children; 6392 for (let i = 0; i < children.length; i++) { 6393 const child = children[i]; 6394 if (child.label === label || label instanceof RegExp && label.test(child.label)) 6395 return child; 6396 } 6397 if (deep) { 6398 for (let i = 0; i < children.length; i++) { 6399 const child = children[i]; 6400 const found = child.getChildByLabel(label, true); 6401 if (found) { 6402 return found; 6403 } 6404 } 6405 } 6406 return null; 6407 }, 6408 getChildrenByLabel(label, deep = false, out = []) { 6409 const children = this.children; 6410 for (let i = 0; i < children.length; i++) { 6411 const child = children[i]; 6412 if (child.label === label || label instanceof RegExp && label.test(child.label)) { 6413 out.push(child); 6414 } 6415 } 6416 if (deep) { 6417 for (let i = 0; i < children.length; i++) { 6418 children[i].getChildrenByLabel(label, true, out); 6419 } 6420 } 6421 return out; 6422 } 6423}; 6424 6425const matrixPool = BigPool.getPool(Matrix); 6426const boundsPool = BigPool.getPool(Bounds); 6427 6428const tempMatrix$2 = new Matrix(); 6429const getFastGlobalBoundsMixin = { 6430 getFastGlobalBounds(factorRenderLayers, bounds) { 6431 bounds || (bounds = new Bounds()); 6432 bounds.clear(); 6433 this._getGlobalBoundsRecursive(!!factorRenderLayers, bounds, this.parentRenderLayer); 6434 if (!bounds.isValid) { 6435 bounds.set(0, 0, 0, 0); 6436 } 6437 const renderGroup = this.renderGroup || this.parentRenderGroup; 6438 bounds.applyMatrix(renderGroup.worldTransform); 6439 return bounds; 6440 }, 6441 _getGlobalBoundsRecursive(factorRenderLayers, bounds, currentLayer) { 6442 let localBounds = bounds; 6443 if (factorRenderLayers && this.parentRenderLayer && this.parentRenderLayer !== currentLayer) 6444 return; 6445 if (this.localDisplayStatus !== 7 || !this.measurable) { 6446 return; 6447 } 6448 const manageEffects = !!this.effects.length; 6449 if (this.renderGroup || manageEffects) { 6450 localBounds = boundsPool.get().clear(); 6451 } 6452 if (this.boundsArea) { 6453 bounds.addRect(this.boundsArea, this.worldTransform); 6454 } else { 6455 if (this.renderPipeId) {
vendor: 8,881 bytes, lines 6456-6748
6456 const viewBounds = this.bounds; 6457 localBounds.addFrame( 6458 viewBounds.minX, 6459 viewBounds.minY, 6460 viewBounds.maxX, 6461 viewBounds.maxY, 6462 this.groupTransform 6463 ); 6464 } 6465 const children = this.children; 6466 for (let i = 0; i < children.length; i++) { 6467 children[i]._getGlobalBoundsRecursive(factorRenderLayers, localBounds, currentLayer); 6468 } 6469 } 6470 if (manageEffects) { 6471 let advanced = false; 6472 const renderGroup = this.renderGroup || this.parentRenderGroup; 6473 for (let i = 0; i < this.effects.length; i++) { 6474 if (this.effects[i].addBounds) { 6475 if (!advanced) { 6476 advanced = true; 6477 localBounds.applyMatrix(renderGroup.worldTransform); 6478 } 6479 this.effects[i].addBounds(localBounds, true); 6480 } 6481 } 6482 if (advanced) { 6483 localBounds.applyMatrix(renderGroup.worldTransform.copyTo(tempMatrix$2).invert()); 6484 } 6485 bounds.addBounds(localBounds); 6486 boundsPool.return(localBounds); 6487 } else if (this.renderGroup) { 6488 bounds.addBounds(localBounds, this.relativeGroupTransform); 6489 boundsPool.return(localBounds); 6490 } 6491 } 6492}; 6493 6494function getGlobalBounds(target, skipUpdateTransform, bounds) { 6495 bounds.clear(); 6496 let parentTransform; 6497 let pooledMatrix; 6498 if (target.parent) { 6499 if (!skipUpdateTransform) { 6500 pooledMatrix = matrixPool.get().identity(); 6501 parentTransform = updateTransformBackwards(target, pooledMatrix); 6502 } else { 6503 parentTransform = target.parent.worldTransform; 6504 } 6505 } else { 6506 parentTransform = Matrix.IDENTITY; 6507 } 6508 _getGlobalBounds(target, bounds, parentTransform, skipUpdateTransform); 6509 if (pooledMatrix) { 6510 matrixPool.return(pooledMatrix); 6511 } 6512 if (!bounds.isValid) { 6513 bounds.set(0, 0, 0, 0); 6514 } 6515 return bounds; 6516} 6517function _getGlobalBounds(target, bounds, parentTransform, skipUpdateTransform) { 6518 if (!target.visible || !target.measurable) 6519 return; 6520 let worldTransform; 6521 if (!skipUpdateTransform) { 6522 target.updateLocalTransform(); 6523 worldTransform = matrixPool.get(); 6524 worldTransform.appendFrom(target.localTransform, parentTransform); 6525 } else { 6526 worldTransform = target.worldTransform; 6527 } 6528 const parentBounds = bounds; 6529 const preserveBounds = !!target.effects.length; 6530 if (preserveBounds) { 6531 bounds = boundsPool.get().clear(); 6532 } 6533 if (target.boundsArea) { 6534 bounds.addRect(target.boundsArea, worldTransform); 6535 } else { 6536 const renderableBounds = target.bounds; 6537 if (renderableBounds && !renderableBounds.isEmpty()) { 6538 bounds.matrix = worldTransform; 6539 bounds.addBounds(renderableBounds); 6540 } 6541 for (let i = 0; i < target.children.length; i++) { 6542 _getGlobalBounds(target.children[i], bounds, worldTransform, skipUpdateTransform); 6543 } 6544 } 6545 if (preserveBounds) { 6546 for (let i = 0; i < target.effects.length; i++) { 6547 target.effects[i].addBounds?.(bounds); 6548 } 6549 parentBounds.addBounds(bounds, Matrix.IDENTITY); 6550 boundsPool.return(bounds); 6551 } 6552 if (!skipUpdateTransform) { 6553 matrixPool.return(worldTransform); 6554 } 6555} 6556function updateTransformBackwards(target, parentTransform) { 6557 const parent = target.parent; 6558 if (parent) { 6559 updateTransformBackwards(parent, parentTransform); 6560 parent.updateLocalTransform(); 6561 parentTransform.append(parent.localTransform); 6562 } 6563 return parentTransform; 6564} 6565 6566function multiplyHexColors(color1, color2) { 6567 if (color1 === 16777215 || !color2) 6568 return color2; 6569 if (color2 === 16777215 || !color1) 6570 return color1; 6571 const r1 = color1 >> 16 & 255; 6572 const g1 = color1 >> 8 & 255; 6573 const b1 = color1 & 255; 6574 const r2 = color2 >> 16 & 255; 6575 const g2 = color2 >> 8 & 255; 6576 const b2 = color2 & 255; 6577 const r = r1 * r2 / 255 | 0; 6578 const g = g1 * g2 / 255 | 0; 6579 const b = b1 * b2 / 255 | 0; 6580 return (r << 16) + (g << 8) + b; 6581} 6582 6583const WHITE_BGR = 16777215; 6584function multiplyColors(localBGRColor, parentBGRColor) { 6585 if (localBGRColor === WHITE_BGR) { 6586 return parentBGRColor; 6587 } 6588 if (parentBGRColor === WHITE_BGR) { 6589 return localBGRColor; 6590 } 6591 return multiplyHexColors(localBGRColor, parentBGRColor); 6592} 6593 6594function bgr2rgb(color) { 6595 return ((color & 255) << 16) + (color & 65280) + (color >> 16 & 255); 6596} 6597const getGlobalMixin = { 6598 getGlobalAlpha(skipUpdate) { 6599 if (skipUpdate) { 6600 if (this.renderGroup) { 6601 return this.renderGroup.worldAlpha; 6602 } 6603 if (this.parentRenderGroup) { 6604 return this.parentRenderGroup.worldAlpha * this.alpha; 6605 } 6606 return this.alpha; 6607 } 6608 let alpha = this.alpha; 6609 let current = this.parent; 6610 while (current) { 6611 alpha *= current.alpha; 6612 current = current.parent; 6613 } 6614 return alpha; 6615 }, 6616 getGlobalTransform(matrix = new Matrix(), skipUpdate) { 6617 if (skipUpdate) { 6618 return matrix.copyFrom(this.worldTransform); 6619 } 6620 this.updateLocalTransform(); 6621 const parentTransform = updateTransformBackwards(this, matrixPool.get().identity()); 6622 matrix.appendFrom(this.localTransform, parentTransform); 6623 matrixPool.return(parentTransform); 6624 return matrix; 6625 }, 6626 getGlobalTint(skipUpdate) { 6627 if (skipUpdate) { 6628 if (this.renderGroup) { 6629 return bgr2rgb(this.renderGroup.worldColor); 6630 } 6631 if (this.parentRenderGroup) { 6632 return bgr2rgb( 6633 multiplyColors(this.localColor, this.parentRenderGroup.worldColor) 6634 ); 6635 } 6636 return this.tint; 6637 } 6638 let color = this.localColor; 6639 let parent = this.parent; 6640 while (parent) { 6641 color = multiplyColors(color, parent.localColor); 6642 parent = parent.parent; 6643 } 6644 return bgr2rgb(color); 6645 } 6646}; 6647 6648function getLocalBounds(target, bounds, relativeMatrix) { 6649 bounds.clear(); 6650 relativeMatrix || (relativeMatrix = Matrix.IDENTITY); 6651 _getLocalBounds(target, bounds, relativeMatrix, target, true); 6652 if (!bounds.isValid) { 6653 bounds.set(0, 0, 0, 0); 6654 } 6655 return bounds; 6656} 6657function _getLocalBounds(target, bounds, parentTransform, rootContainer, isRoot) { 6658 let relativeTransform; 6659 if (!isRoot) { 6660 if (!target.visible || !target.measurable) 6661 return; 6662 target.updateLocalTransform(); 6663 const localTransform = target.localTransform; 6664 relativeTransform = matrixPool.get(); 6665 relativeTransform.appendFrom(localTransform, parentTransform); 6666 } else { 6667 relativeTransform = matrixPool.get(); 6668 relativeTransform = parentTransform.copyTo(relativeTransform); 6669 } 6670 const parentBounds = bounds; 6671 const preserveBounds = !!target.effects.length; 6672 if (preserveBounds) { 6673 bounds = boundsPool.get().clear(); 6674 } 6675 if (target.boundsArea) { 6676 bounds.addRect(target.boundsArea, relativeTransform); 6677 } else { 6678 if (target.renderPipeId) { 6679 bounds.matrix = relativeTransform; 6680 bounds.addBounds(target.bounds); 6681 } 6682 const children = target.children; 6683 for (let i = 0; i < children.length; i++) { 6684 _getLocalBounds(children[i], bounds, relativeTransform, rootContainer, false); 6685 } 6686 } 6687 if (preserveBounds) { 6688 for (let i = 0; i < target.effects.length; i++) { 6689 target.effects[i].addLocalBounds?.(bounds, rootContainer); 6690 } 6691 parentBounds.addBounds(bounds, Matrix.IDENTITY); 6692 boundsPool.return(bounds); 6693 } 6694 matrixPool.return(relativeTransform); 6695} 6696 6697function checkChildrenDidChange(container, previousData) { 6698 const children = container.children; 6699 for (let i = 0; i < children.length; i++) { 6700 const child = children[i]; 6701 const uid = child.uid; 6702 const didChange = (child._didViewChangeTick & 65535) << 16 | child._didContainerChangeTick & 65535; 6703 const index = previousData.index; 6704 if (previousData.data[index] !== uid || previousData.data[index + 1] !== didChange) { 6705 previousData.data[previousData.index] = uid; 6706 previousData.data[previousData.index + 1] = didChange; 6707 previousData.didChange = true; 6708 } 6709 previousData.index = index + 2; 6710 if (child.children.length) { 6711 checkChildrenDidChange(child, previousData); 6712 } 6713 } 6714 return previousData.didChange; 6715} 6716 6717const tempMatrix$1 = new Matrix(); 6718const measureMixin = { 6719 _localBoundsCacheId: -1, 6720 _localBoundsCacheData: null, 6721 _setWidth(value, localWidth) { 6722 const sign = Math.sign(this.scale.x) || 1; 6723 if (localWidth !== 0) { 6724 this.scale.x = value / localWidth * sign; 6725 } else { 6726 this.scale.x = sign; 6727 } 6728 }, 6729 _setHeight(value, localHeight) { 6730 const sign = Math.sign(this.scale.y) || 1; 6731 if (localHeight !== 0) { 6732 this.scale.y = value / localHeight * sign; 6733 } else { 6734 this.scale.y = sign; 6735 } 6736 }, 6737 getLocalBounds() { 6738 if (!this._localBoundsCacheData) { 6739 this._localBoundsCacheData = { 6740 data: [], 6741 index: 1, 6742 didChange: false, 6743 localBounds: new Bounds() 6744 }; 6745 } 6746 const localBoundsCacheData = this._localBoundsCacheData; 6747 localBoundsCacheData.index = 1; 6748 localBoundsCacheData.didChange = false;
6749 if (localBoundsCacheData.data[0] !== this._didViewChangeTick) { 6750 localBoundsCacheData.didChange = true; 6751 localBoundsCacheData.data[0] = this._didViewChangeTick; 6752 } 6753 checkChildrenDidChange(this, localBoundsCacheData); 6754 if (localBoundsCacheData.didChange) { 6755 getLocalBounds(this, localBoundsCacheData.localBounds, tempMatrix$1); 6756 } 6757 return localBoundsCacheData.localBounds; 6758 }, 6759 getBounds(skipUpdate, bounds) { 6760 return getGlobalBounds(this, skipUpdate, bounds || new Bounds()); 6761 } 6762}; 6763 6764const onRenderMixin = { 6765 _onRender: null, 6766 set onRender(func) { 6767 const renderGroup = this.renderGroup || this.parentRenderGroup; 6768 if (!func) { 6769 if (this._onRender) { 6770 renderGroup?.removeOnRender(this); 6771 } 6772 this._onRender = null; 6773 return; 6774 } 6775 if (!this._onRender) { 6776 renderGroup?.addOnRender(this); 6777 } 6778 this._onRender = func; 6779 }, 6780 get onRender() { 6781 return this._onRender; 6782 } 6783}; 6784 6785const sortMixin = { 6786 _zIndex: 0, 6787 sortDirty: false, 6788 sortableChildren: false, 6789 get zIndex() { 6790 return this._zIndex; 6791 }, 6792 set zIndex(value) { 6793 if (this._zIndex === value) 6794 return; 6795 this._zIndex = value; 6796 this.depthOfChildModified(); 6797 }, 6798 depthOfChildModified() { 6799 if (this.parent) { 6800 this.parent.sortableChildren = true; 6801 this.parent.sortDirty = true; 6802 } 6803 if (this.parentRenderGroup) { 6804 this.parentRenderGroup.structureDidChange = true; 6805 } 6806 }, 6807 sortChildren() { 6808 if (!this.sortDirty) 6809 return; 6810 this.sortDirty = false; 6811 this.children.sort(sortChildren); 6812 } 6813}; 6814function sortChildren(a, b) { 6815 return a._zIndex - b._zIndex; 6816} 6817 6818const toLocalGlobalMixin = { 6819 getGlobalPosition(point = new Point(), skipUpdate = false) { 6820 if (this.parent) { 6821 this.parent.toGlobal(this._position, point, skipUpdate); 6822 } else { 6823 point.x = this._position.x; 6824 point.y = this._position.y; 6825 } 6826 return point; 6827 }, 6828 toGlobal(position, point, skipUpdate = false) { 6829 const globalMatrix = this.getGlobalTransform(matrixPool.get(), skipUpdate); 6830 point = globalMatrix.apply(position, point); 6831 matrixPool.return(globalMatrix); 6832 return point; 6833 }, 6834 toLocal(position, from, point, skipUpdate) { 6835 if (from) { 6836 position = from.toGlobal(position, point, skipUpdate); 6837 } 6838 const globalMatrix = this.getGlobalTransform(matrixPool.get(), skipUpdate); 6839 point = globalMatrix.applyInverse(position, point); 6840 matrixPool.return(globalMatrix); 6841 return point; 6842 } 6843}; 6844 6845class InstructionSet { 6846 constructor() { 6847 /** a unique id for this instruction set used through the renderer */ 6848 this.uid = uid("instructionSet"); 6849 /** the array of instructions */ 6850 this.instructions = []; 6851 /** the actual size of the array (any instructions passed this should be ignored) */ 6852 this.instructionSize = 0; 6853 this.renderables = []; 6854 /** used by the garbage collector to track when the instruction set was last used */ 6855 this.gcTick = 0; 6856 } 6857 /** reset the instruction set so it can be reused set size back to 0 */ 6858 reset() { 6859 this.instructionSize = 0; 6860 } 6861 /** 6862 * Destroy the instruction set, clearing the instructions and renderables. 6863 * @internal 6864 */ 6865 destroy() { 6866 this.instructions.length = 0; 6867 this.renderables.length = 0; 6868 this.renderPipes = null; 6869 this.gcTick = 0; 6870 } 6871 /** 6872 * Add an instruction to the set 6873 * @param instruction - add an instruction to the set 6874 */ 6875 add(instruction) { 6876 this.instructions[this.instructionSize++] = instruction; 6877 } 6878 /** 6879 * Log the instructions to the console (for debugging) 6880 * @internal 6881 */ 6882 log() { 6883 this.instructions.length = this.instructionSize; 6884 console.table(this.instructions, ["type", "action"]); 6885 } 6886} 6887 6888let count = 0; 6889class TexturePoolClass { 6890 /** 6891 * @param textureOptions - options that will be passed to BaseRenderTexture constructor 6892 * @param {SCALE_MODE} [textureOptions.scaleMode] - See {@link SCALE_MODE} for possible values. 6893 */ 6894 constructor(textureOptions) { 6895 this._poolKeyHash = /* @__PURE__ */ Object.create(null); 6896 this._texturePool = {}; 6897 this.textureOptions = textureOptions || {}; 6898 this.enableFullScreen = false;
6899 this.textureStyle = new TextureStyle(this.textureOptions); 6900 } 6901 /** 6902 * Creates texture with params that were specified in pool constructor. 6903 * @param pixelWidth - Width of texture in pixels. 6904 * @param pixelHeight - Height of texture in pixels. 6905 * @param antialias 6906 */ 6907 createTexture(pixelWidth, pixelHeight, antialias) { 6908 const textureSource = new TextureSource({ 6909 ...this.textureOptions, 6910 width: pixelWidth, 6911 height: pixelHeight, 6912 resolution: 1, 6913 antialias, 6914 autoGarbageCollect: false 6915 }); 6916 return new Texture({ 6917 source: textureSource, 6918 label: `texturePool_${count++}` 6919 }); 6920 } 6921 /** 6922 * Gets a Power-of-Two render texture or fullScreen texture 6923 * @param frameWidth - The minimum width of the render texture. 6924 * @param frameHeight - The minimum height of the render texture. 6925 * @param resolution - The resolution of the render texture. 6926 * @param antialias 6927 * @returns The new render texture. 6928 */ 6929 getOptimalTexture(frameWidth, frameHeight, resolution = 1, antialias) { 6930 let po2Width = Math.ceil(frameWidth * resolution - 1e-6); 6931 let po2Height = Math.ceil(frameHeight * resolution - 1e-6); 6932 po2Width = nextPow2(po2Width); 6933 po2Height = nextPow2(po2Height); 6934 const key = (po2Width << 17) + (po2Height << 1) + (antialias ? 1 : 0); 6935 if (!this._texturePool[key]) { 6936 this._texturePool[key] = []; 6937 } 6938 let texture = this._texturePool[key].pop(); 6939 if (!texture) { 6940 texture = this.createTexture(po2Width, po2Height, antialias); 6941 } 6942 texture.source._resolution = resolution; 6943 texture.source.width = po2Width / resolution; 6944 texture.source.height = po2Height / resolution; 6945 texture.source.pixelWidth = po2Width; 6946 texture.source.pixelHeight = po2Height; 6947 texture.frame.x = 0; 6948 texture.frame.y = 0; 6949 texture.frame.width = frameWidth; 6950 texture.frame.height = frameHeight; 6951 texture.updateUvs(); 6952 this._poolKeyHash[texture.uid] = key; 6953 return texture; 6954 } 6955 /** 6956 * Gets extra texture of the same size as input renderTexture 6957 * @param texture - The texture to check what size it is. 6958 * @param antialias - Whether to use antialias. 6959 * @returns A texture that is a power of two 6960 */ 6961 getSameSizeTexture(texture, antialias = false) { 6962 const source = texture.source; 6963 return this.getOptimalTexture(texture.width, texture.height, source._resolution, antialias); 6964 } 6965 /** 6966 * Place a render texture back into the pool. Optionally reset the style of the texture to the default texture style. 6967 * useful if you modified the style of the texture after getting it from the pool. 6968 * @param renderTexture - The renderTexture to free 6969 * @param resetStyle - Whether to reset the style of the texture to the default texture style 6970 */ 6971 returnTexture(renderTexture, resetStyle = false) { 6972 const key = this._poolKeyHash[renderTexture.uid]; 6973 if (resetStyle) { 6974 renderTexture.source.style = this.textureStyle; 6975 } 6976 this._texturePool[key].push(renderTexture); 6977 } 6978 /** 6979 * Clears the pool. 6980 * @param destroyTextures - Destroy all stored textures. 6981 */ 6982 clear(destroyTextures) { 6983 destroyTextures = destroyTextures !== false; 6984 if (destroyTextures) { 6985 for (const i in this._texturePool) { 6986 const textures = this._texturePool[i]; 6987 if (textures) { 6988 for (let j = 0; j < textures.length; j++) { 6989 textures[j].destroy(true); 6990 } 6991 } 6992 } 6993 } 6994 this._texturePool = {}; 6995 } 6996} 6997const TexturePool = new TexturePoolClass(); 6998GlobalResourceRegistry.register(TexturePool); 6999 7000class RenderGroup { 7001 constructor() { 7002 this.renderPipeId = "renderGroup"; 7003 this.root = null; 7004 this.canBundle = false; 7005 this.renderGroupParent = null; 7006 this.renderGroupChildren = []; 7007 this.worldTransform = new Matrix(); 7008 this.worldColorAlpha = 4294967295; 7009 this.worldColor = 16777215; 7010 this.worldAlpha = 1; 7011 // these updates are transform changes.. 7012 this.childrenToUpdate = /* @__PURE__ */ Object.create(null); 7013 this.updateTick = 0; 7014 this.gcTick = 0; 7015 // these update are renderable changes.. 7016 this.childrenRenderablesToUpdate = { list: [], index: 0 }; 7017 // other 7018 this.structureDidChange = true; 7019 this.instructionSet = new InstructionSet(); 7020 this._onRenderContainers = []; 7021 /** 7022 * Indicates if the cached texture needs to be updated. 7023 * @default true 7024 */ 7025 this.textureNeedsUpdate = true; 7026 /** 7027 * Indicates if the container should be cached as a texture. 7028 * @default false 7029 */ 7030 this.isCachedAsTexture = false;
vendor: 8,393 bytes, lines 7031-7283
7031 this._matrixDirty = 7; 7032 } 7033 init(root) { 7034 this.root = root; 7035 if (root._onRender) 7036 this.addOnRender(root); 7037 root.didChange = true; 7038 const children = root.children; 7039 for (let i = 0; i < children.length; i++) { 7040 const child = children[i]; 7041 child._updateFlags = 15; 7042 this.addChild(child); 7043 } 7044 } 7045 enableCacheAsTexture(options = {}) { 7046 this.textureOptions = options; 7047 this.isCachedAsTexture = true; 7048 this.textureNeedsUpdate = true; 7049 } 7050 disableCacheAsTexture() { 7051 this.isCachedAsTexture = false; 7052 if (this.texture) { 7053 TexturePool.returnTexture(this.texture, true); 7054 this.texture = null; 7055 } 7056 } 7057 updateCacheTexture() { 7058 this.textureNeedsUpdate = true; 7059 const cachedParent = this._parentCacheAsTextureRenderGroup; 7060 if (cachedParent && !cachedParent.textureNeedsUpdate) { 7061 cachedParent.updateCacheTexture(); 7062 } 7063 } 7064 reset() { 7065 this.renderGroupChildren.length = 0; 7066 for (const i in this.childrenToUpdate) { 7067 const childrenAtDepth = this.childrenToUpdate[i]; 7068 childrenAtDepth.list.fill(null); 7069 childrenAtDepth.index = 0; 7070 } 7071 this.childrenRenderablesToUpdate.index = 0; 7072 this.childrenRenderablesToUpdate.list.fill(null); 7073 this.root = null; 7074 this.updateTick = 0; 7075 this.structureDidChange = true; 7076 this._onRenderContainers.length = 0; 7077 this.renderGroupParent = null; 7078 this.disableCacheAsTexture(); 7079 } 7080 get localTransform() { 7081 return this.root.localTransform; 7082 } 7083 addRenderGroupChild(renderGroupChild) { 7084 if (renderGroupChild.renderGroupParent) { 7085 renderGroupChild.renderGroupParent._removeRenderGroupChild(renderGroupChild); 7086 } 7087 renderGroupChild.renderGroupParent = this; 7088 this.renderGroupChildren.push(renderGroupChild); 7089 } 7090 _removeRenderGroupChild(renderGroupChild) { 7091 const index = this.renderGroupChildren.indexOf(renderGroupChild); 7092 if (index > -1) { 7093 this.renderGroupChildren.splice(index, 1); 7094 } 7095 renderGroupChild.renderGroupParent = null; 7096 } 7097 addChild(child) { 7098 this.structureDidChange = true; 7099 child.parentRenderGroup = this; 7100 child.updateTick = -1; 7101 if (child.parent === this.root) { 7102 child.relativeRenderGroupDepth = 1; 7103 } else { 7104 child.relativeRenderGroupDepth = child.parent.relativeRenderGroupDepth + 1; 7105 } 7106 child.didChange = true; 7107 this.onChildUpdate(child); 7108 if (child.renderGroup) { 7109 this.addRenderGroupChild(child.renderGroup); 7110 return; 7111 } 7112 if (child._onRender) 7113 this.addOnRender(child); 7114 const children = child.children; 7115 for (let i = 0; i < children.length; i++) { 7116 this.addChild(children[i]); 7117 } 7118 } 7119 removeChild(child) { 7120 this.structureDidChange = true; 7121 if (child._onRender) { 7122 if (!child.renderGroup) { 7123 this.removeOnRender(child); 7124 } 7125 } 7126 child.parentRenderGroup = null; 7127 if (child.renderGroup) { 7128 this._removeRenderGroupChild(child.renderGroup); 7129 return; 7130 } 7131 const children = child.children; 7132 for (let i = 0; i < children.length; i++) { 7133 this.removeChild(children[i]); 7134 } 7135 } 7136 removeChildren(children) { 7137 for (let i = 0; i < children.length; i++) { 7138 this.removeChild(children[i]); 7139 } 7140 } 7141 onChildUpdate(child) { 7142 let childrenToUpdate = this.childrenToUpdate[child.relativeRenderGroupDepth]; 7143 if (!childrenToUpdate) { 7144 childrenToUpdate = this.childrenToUpdate[child.relativeRenderGroupDepth] = { 7145 index: 0, 7146 list: [] 7147 }; 7148 } 7149 childrenToUpdate.list[childrenToUpdate.index++] = child; 7150 } 7151 updateRenderable(renderable) { 7152 if (renderable.globalDisplayStatus < 7) 7153 return; 7154 this.instructionSet.renderPipes[renderable.renderPipeId].updateRenderable(renderable); 7155 renderable.didViewUpdate = false; 7156 } 7157 onChildViewUpdate(child) { 7158 this.childrenRenderablesToUpdate.list[this.childrenRenderablesToUpdate.index++] = child; 7159 } 7160 get isRenderable() { 7161 return this.root.localDisplayStatus === 7 && this.worldAlpha > 0; 7162 } 7163 /** 7164 * adding a container to the onRender list will make sure the user function 7165 * passed in to the user defined 'onRender` callBack 7166 * @param container - the container to add to the onRender list 7167 */ 7168 addOnRender(container) { 7169 this._onRenderContainers.push(container); 7170 } 7171 removeOnRender(container) { 7172 this._onRenderContainers.splice(this._onRenderContainers.indexOf(container), 1); 7173 } 7174 runOnRender(renderer) { 7175 for (let i = 0; i < this._onRenderContainers.length; i++) { 7176 this._onRenderContainers[i]._onRender(renderer); 7177 } 7178 } 7179 destroy() { 7180 this.disableCacheAsTexture(); 7181 this.renderGroupParent = null; 7182 this.root = null; 7183 this.childrenRenderablesToUpdate = null; 7184 this.childrenToUpdate = null; 7185 this.renderGroupChildren = null; 7186 this._onRenderContainers = null; 7187 this.instructionSet = null; 7188 } 7189 getChildren(out = []) { 7190 const children = this.root.children; 7191 for (let i = 0; i < children.length; i++) { 7192 this._getChildren(children[i], out); 7193 } 7194 return out; 7195 } 7196 _getChildren(container, out = []) { 7197 out.push(container); 7198 if (container.renderGroup) 7199 return out; 7200 const children = container.children; 7201 for (let i = 0; i < children.length; i++) { 7202 this._getChildren(children[i], out); 7203 } 7204 return out; 7205 } 7206 invalidateMatrices() { 7207 this._matrixDirty = 7; 7208 } 7209 /** 7210 * Returns the inverse of the world transform matrix. 7211 * @returns {Matrix} The inverse of the world transform matrix. 7212 */ 7213 get inverseWorldTransform() { 7214 if ((this._matrixDirty & 1) === 0) 7215 return this._inverseWorldTransform; 7216 this._matrixDirty &= -2; 7217 this._inverseWorldTransform || (this._inverseWorldTransform = new Matrix()); 7218 return this._inverseWorldTransform.copyFrom(this.worldTransform).invert(); 7219 } 7220 /** 7221 * Returns the inverse of the texture offset transform matrix. 7222 * @returns {Matrix} The inverse of the texture offset transform matrix. 7223 */ 7224 get textureOffsetInverseTransform() { 7225 if ((this._matrixDirty & 2) === 0) 7226 return this._textureOffsetInverseTransform; 7227 this._matrixDirty &= -3; 7228 this._textureOffsetInverseTransform || (this._textureOffsetInverseTransform = new Matrix()); 7229 return this._textureOffsetInverseTransform.copyFrom(this.inverseWorldTransform).translate( 7230 -this._textureBounds.x, 7231 -this._textureBounds.y 7232 ); 7233 } 7234 /** 7235 * Returns the inverse of the parent texture transform matrix. 7236 * This is used to properly transform coordinates when rendering into cached textures. 7237 * @returns {Matrix} The inverse of the parent texture transform matrix. 7238 */ 7239 get inverseParentTextureTransform() { 7240 if ((this._matrixDirty & 4) === 0) 7241 return this._inverseParentTextureTransform; 7242 this._matrixDirty &= -5; 7243 const parentCacheAsTexture = this._parentCacheAsTextureRenderGroup; 7244 if (parentCacheAsTexture) { 7245 this._inverseParentTextureTransform || (this._inverseParentTextureTransform = new Matrix()); 7246 return this._inverseParentTextureTransform.copyFrom(this.worldTransform).prepend(parentCacheAsTexture.inverseWorldTransform).translate( 7247 -parentCacheAsTexture._textureBounds.x, 7248 -parentCacheAsTexture._textureBounds.y 7249 ); 7250 } 7251 return this.worldTransform; 7252 } 7253 /** 7254 * Returns a matrix that transforms coordinates to the correct coordinate space of the texture being rendered to. 7255 * This is the texture offset inverse transform of the closest parent RenderGroup that is cached as a texture. 7256 * @returns {Matrix | null} The transform matrix for the cached texture coordinate space, 7257 * or null if no parent is cached as texture. 7258 */ 7259 get cacheToLocalTransform() { 7260 if (this.isCachedAsTexture) { 7261 return this.textureOffsetInverseTransform; 7262 } 7263 if (!this._parentCacheAsTextureRenderGroup) 7264 return null; 7265 return this._parentCacheAsTextureRenderGroup.textureOffsetInverseTransform; 7266 } 7267} 7268 7269function assignWithIgnore(target, options, ignore = {}) { 7270 for (const key in options) { 7271 if (!ignore[key] && options[key] !== void 0) { 7272 target[key] = options[key]; 7273 } 7274 } 7275} 7276 7277const defaultSkew = new ObservablePoint(null); 7278const defaultPivot = new ObservablePoint(null); 7279const defaultScale = new ObservablePoint(null, 1, 1); 7280const defaultOrigin = new ObservablePoint(null); 7281const UPDATE_COLOR = 1; 7282const UPDATE_BLEND = 2; 7283const UPDATE_VISIBLE = 4;
7284class Container extends EventEmitter { 7285 constructor(options = {}) { 7286 super(); 7287 /** 7288 * unique id for this container 7289 * @internal 7290 */ 7291 this.uid = uid("renderable"); 7292 /** @private */ 7293 this._updateFlags = 15; 7294 // the render group this container owns 7295 /** @private */ 7296 this.renderGroup = null; 7297 // the render group this container belongs to 7298 /** @private */ 7299 this.parentRenderGroup = null; 7300 // the index of the container in the render group 7301 /** @private */ 7302 this.parentRenderGroupIndex = 0; 7303 // set to true if the container has changed. It is reset once the changes have been applied 7304 // by the transform system 7305 // its here to stop ensure that when things change, only one update gets registers with the transform system 7306 /** @private */ 7307 this.didChange = false; 7308 // same as above, but for the renderable 7309 /** @private */ 7310 this.didViewUpdate = false; 7311 // how deep is the container relative to its render group.. 7312 // unless the element is the root render group - it will be relative to its parent 7313 /** @private */ 7314 this.relativeRenderGroupDepth = 0; 7315 /** 7316 * The array of children of this container. Each child must be a Container or extend from it. 7317 * 7318 * The array is read-only, but its contents can be modified using Container methods. 7319 * @example 7320 * ```ts 7321 * // Access children 7322 * const firstChild = container.children[0]; 7323 * const lastChild = container.children[container.children.length - 1]; 7324 * ``` 7325 * @readonly 7326 * @see {@link Container#addChild} For adding children 7327 * @see {@link Container#removeChild} For removing children 7328 */ 7329 this.children = []; 7330 /** 7331 * The display object container that contains this display object. 7332 * This represents the parent-child relationship in the display tree. 7333 * @example 7334 * ```ts 7335 * // Basic parent access 7336 * const parent = sprite.parent; 7337 * 7338 * // Walk up the tree 7339 * let current = sprite; 7340 * while (current.parent) { 7341 * console.log('Level up:', current.parent.constructor.name); 7342 * current = current.parent; 7343 * } 7344 * ``` 7345 * @readonly 7346 * @see {@link Container#addChild} For adding to a parent 7347 * @see {@link Container#removeChild} For removing from parent 7348 */ 7349 this.parent = null; 7350 // used internally for changing up the render order.. mainly for masks and filters 7351 // TODO setting this should cause a rebuild?? 7352 /** @private */ 7353 this.includeInBuild = true; 7354 /** @private */ 7355 this.measurable = true; 7356 /** @private */ 7357 this.isSimple = true; 7358 /** 7359 * The RenderLayer this container belongs to, if any. 7360 * If it belongs to a RenderLayer, it will be rendered from the RenderLayer's position in the scene. 7361 * @readonly 7362 * @advanced 7363 */ 7364 this.parentRenderLayer = null; 7365 // / /////////////Transform related props////////////// 7366 // used by the transform system to check if a container needs to be updated that frame 7367 // if the tick matches the current transform system tick, it is not updated again 7368 /** @internal */ 7369 this.updateTick = -1; 7370 /** 7371 * Current transform of the object based on local factors: position, scale, other stuff. 7372 * This matrix represents the local transformation without any parent influence. 7373 * @example 7374 * ```ts 7375 * // Basic transform access 7376 * const localMatrix = sprite.localTransform; 7377 * console.log(localMatrix.toString()); 7378 * ``` 7379 * @readonly 7380 * @see {@link Container#worldTransform} For global transform 7381 * @see {@link Container#groupTransform} For render group transform 7382 */ 7383 this.localTransform = new Matrix(); 7384 /** 7385 * The relative group transform is a transform relative to the render group it belongs too. It will include all parent 7386 * transforms and up to the render group (think of it as kind of like a stage - but the stage can be nested). 7387 * If this container is is self a render group matrix will be relative to its parent render group 7388 * @readonly 7389 * @advanced 7390 */ 7391 this.relativeGroupTransform = new Matrix(); 7392 /** 7393 * The group transform is a transform relative to the render group it belongs too. 7394 * If this container is render group then this will be an identity matrix. other wise it 7395 * will be the same as the relativeGroupTransform. 7396 * Use this value when actually rendering things to the screen 7397 * @readonly 7398 * @advanced 7399 */ 7400 this.groupTransform = this.relativeGroupTransform; 7401 /** 7402 * Whether this object has been destroyed. If true, the object should no longer be used. 7403 * After an object is destroyed, all of its functionality is disabled and references are removed. 7404 * @example 7405 * ```ts 7406 * // Cleanup with destroy 7407 * sprite.destroy(); 7408 * console.log(sprite.destroyed); // true 7409 * ``` 7410 * @default false 7411 * @see {@link Container#destroy} For destroying objects 7412 */ 7413 this.destroyed = false;
vendor: 5,870 bytes, lines 7414-7605
7414 // transform data.. 7415 /** 7416 * The coordinate of the object relative to the local coordinates of the parent. 7417 * @internal 7418 */ 7419 this._position = new ObservablePoint(this, 0, 0); 7420 /** 7421 * The scale factor of the object. 7422 * @internal 7423 */ 7424 this._scale = defaultScale; 7425 /** 7426 * The pivot point of the container that it rotates around. 7427 * @internal 7428 */ 7429 this._pivot = defaultPivot; 7430 /** 7431 * The origin point around which the container rotates and scales. 7432 * Unlike pivot, changing origin will not move the container's position. 7433 * @private 7434 */ 7435 this._origin = defaultOrigin; 7436 /** 7437 * The skew amount, on the x and y axis. 7438 * @internal 7439 */ 7440 this._skew = defaultSkew; 7441 /** 7442 * The X-coordinate value of the normalized local X axis, 7443 * the first column of the local transformation matrix without a scale. 7444 * @internal 7445 */ 7446 this._cx = 1; 7447 /** 7448 * The Y-coordinate value of the normalized local X axis, 7449 * the first column of the local transformation matrix without a scale. 7450 * @internal 7451 */ 7452 this._sx = 0; 7453 /** 7454 * The X-coordinate value of the normalized local Y axis, 7455 * the second column of the local transformation matrix without a scale. 7456 * @internal 7457 */ 7458 this._cy = 0; 7459 /** 7460 * The Y-coordinate value of the normalized local Y axis, 7461 * the second column of the local transformation matrix without a scale. 7462 * @internal 7463 */ 7464 this._sy = 1; 7465 /** 7466 * The rotation amount. 7467 * @internal 7468 */ 7469 this._rotation = 0; 7470 // / COLOR related props ////////////// 7471 // color stored as ABGR 7472 /** @internal */ 7473 this.localColor = 16777215; 7474 /** @internal */ 7475 this.localAlpha = 1; 7476 /** @internal */ 7477 this.groupAlpha = 1; 7478 // A 7479 /** @internal */ 7480 this.groupColor = 16777215; 7481 // BGR 7482 /** @internal */ 7483 this.groupColorAlpha = 4294967295; 7484 // ABGR 7485 // / BLEND related props ////////////// 7486 /** @internal */ 7487 this.localBlendMode = "inherit"; 7488 /** @internal */ 7489 this.groupBlendMode = "normal"; 7490 // / VISIBILITY related props ////////////// 7491 // visibility 7492 // 0b11 7493 // first bit is visible, second bit is renderable 7494 /** 7495 * This property holds three bits: culled, visible, renderable 7496 * the third bit represents culling (0 = culled, 1 = not culled) 0b100 7497 * the second bit represents visibility (0 = not visible, 1 = visible) 0b010 7498 * the first bit represents renderable (0 = not renderable, 1 = renderable) 0b001 7499 * @internal 7500 */ 7501 this.localDisplayStatus = 7; 7502 // 0b11 | 0b10 | 0b01 | 0b00 7503 /** @internal */ 7504 this.globalDisplayStatus = 7; 7505 /** 7506 * A value that increments each time the containe is modified 7507 * eg children added, removed etc 7508 * @ignore 7509 */ 7510 this._didContainerChangeTick = 0; 7511 /** 7512 * A value that increments each time the container view is modified 7513 * eg texture swap, geometry change etc 7514 * @ignore 7515 */ 7516 this._didViewChangeTick = 0; 7517 /** 7518 * property that tracks if the container transform has changed 7519 * @ignore 7520 */ 7521 this._didLocalTransformChangeId = -1; 7522 this.effects = []; 7523 assignWithIgnore(this, options, { 7524 children: true, 7525 parent: true, 7526 effects: true 7527 }); 7528 options.children?.forEach((child) => this.addChild(child)); 7529 options.parent?.addChild(this); 7530 } 7531 /** 7532 * Mixes all enumerable properties and methods from a source object to Container. 7533 * @param source - The source of properties and methods to mix in. 7534 * @deprecated since 8.8.0 7535 */ 7536 static mixin(source) { 7537 deprecation("8.8.0", "Container.mixin is deprecated, please use extensions.mixin instead."); 7538 extensions.mixin(Container, source); 7539 } 7540 // = 'default'; 7541 /** 7542 * We now use the _didContainerChangeTick and _didViewChangeTick to track changes 7543 * @deprecated since 8.2.6 7544 * @ignore 7545 */ 7546 set _didChangeId(value) { 7547 this._didViewChangeTick = value >> 12 & 4095; 7548 this._didContainerChangeTick = value & 4095; 7549 } 7550 /** @ignore */ 7551 get _didChangeId() { 7552 return this._didContainerChangeTick & 4095 | (this._didViewChangeTick & 4095) << 12; 7553 } 7554 /** 7555 * Adds one or more children to the container. 7556 * The children will be rendered as part of this container's display list. 7557 * @example 7558 * ```ts 7559 * // Add a single child 7560 * container.addChild(sprite); 7561 * 7562 * // Add multiple children 7563 * container.addChild(background, player, foreground); 7564 * 7565 * // Add with type checking 7566 * const sprite = container.addChild<Sprite>(new Sprite(texture)); 7567 * sprite.tint = 'red'; 7568 * ``` 7569 * @param children - The Container(s) to add to the container 7570 * @returns The first child that was added 7571 * @see {@link Container#removeChild} For removing children 7572 * @see {@link Container#addChildAt} For adding at specific index 7573 */ 7574 addChild(...children) { 7575 if (!this.allowChildren) { 7576 deprecation(v8_0_0, "addChild: Only Containers will be allowed to add children in v8.0.0"); 7577 } 7578 if (children.length > 1) { 7579 for (let i = 0; i < children.length; i++) { 7580 this.addChild(children[i]); 7581 } 7582 return children[0]; 7583 } 7584 const child = children[0]; 7585 const renderGroup = this.renderGroup || this.parentRenderGroup; 7586 if (child.parent === this) { 7587 this.children.splice(this.children.indexOf(child), 1); 7588 this.children.push(child); 7589 if (renderGroup) { 7590 renderGroup.structureDidChange = true; 7591 } 7592 return child; 7593 } 7594 if (child.parent) { 7595 child.parent.removeChild(child); 7596 } 7597 this.children.push(child); 7598 if (this.sortableChildren) 7599 this.sortDirty = true; 7600 child.parent = this; 7601 child.didChange = true; 7602 child._updateFlags = 15; 7603 if (renderGroup) { 7604 renderGroup.addChild(child); 7605 }
7606 this.emit("childAdded", child, this, this.children.length - 1); 7607 child.emit("added", this); 7608 this._didViewChangeTick++; 7609 if (child._zIndex !== 0) { 7610 child.depthOfChildModified(); 7611 } 7612 return child; 7613 } 7614 /** 7615 * Removes one or more children from the container. 7616 * When removing multiple children, events will be triggered for each child in sequence. 7617 * @example 7618 * ```ts 7619 * // Remove a single child 7620 * const removed = container.removeChild(sprite); 7621 * 7622 * // Remove multiple children 7623 * const bg = container.removeChild(background, player, userInterface); 7624 * 7625 * // Remove with type checking 7626 * const sprite = container.removeChild<Sprite>(childSprite); 7627 * sprite.texture = newTexture; 7628 * ``` 7629 * @param children - The Container(s) to remove 7630 * @returns The first child that was removed 7631 * @see {@link Container#addChild} For adding children 7632 * @see {@link Container#removeChildren} For removing multiple children 7633 */ 7634 removeChild(...children) { 7635 if (children.length > 1) { 7636 for (let i = 0; i < children.length; i++) { 7637 this.removeChild(children[i]); 7638 } 7639 return children[0]; 7640 } 7641 const child = children[0]; 7642 const index = this.children.indexOf(child); 7643 if (index > -1) { 7644 this._didViewChangeTick++; 7645 this.children.splice(index, 1); 7646 if (this.renderGroup) { 7647 this.renderGroup.removeChild(child); 7648 } else if (this.parentRenderGroup) { 7649 this.parentRenderGroup.removeChild(child); 7650 } 7651 if (child.parentRenderLayer) { 7652 child.parentRenderLayer.detach(child); 7653 } 7654 child.parent = null; 7655 this.emit("childRemoved", child, this, index); 7656 child.emit("removed", this); 7657 } 7658 return child; 7659 } 7660 /** @ignore */ 7661 _onUpdate(point) { 7662 if (point) { 7663 if (point === this._skew) { 7664 this._updateSkew(); 7665 } 7666 } 7667 this._didContainerChangeTick++; 7668 if (this.didChange) 7669 return; 7670 this.didChange = true; 7671 if (this.parentRenderGroup) { 7672 this.parentRenderGroup.onChildUpdate(this); 7673 } 7674 } 7675 set isRenderGroup(value) { 7676 if (!!this.renderGroup === value) 7677 return; 7678 if (value) { 7679 this.enableRenderGroup(); 7680 } else { 7681 this.disableRenderGroup(); 7682 } 7683 } 7684 /** 7685 * Returns true if this container is a render group. 7686 * This means that it will be rendered as a separate pass, with its own set of instructions 7687 * @advanced 7688 */ 7689 get isRenderGroup() { 7690 return !!this.renderGroup; 7691 } 7692 /** 7693 * Calling this enables a render group for this container. 7694 * This means it will be rendered as a separate set of instructions. 7695 * The transform of the container will also be handled on the GPU rather than the CPU. 7696 * @advanced 7697 */ 7698 enableRenderGroup() { 7699 if (this.renderGroup) 7700 return; 7701 const parentRenderGroup = this.parentRenderGroup; 7702 parentRenderGroup?.removeChild(this); 7703 this.renderGroup = BigPool.get(RenderGroup, this); 7704 this.groupTransform = Matrix.IDENTITY; 7705 parentRenderGroup?.addChild(this); 7706 this._updateIsSimple(); 7707 } 7708 /** 7709 * This will disable the render group for this container. 7710 * @advanced 7711 */ 7712 disableRenderGroup() { 7713 if (!this.renderGroup) 7714 return; 7715 const parentRenderGroup = this.parentRenderGroup; 7716 parentRenderGroup?.removeChild(this); 7717 BigPool.return(this.renderGroup); 7718 this.renderGroup = null; 7719 this.groupTransform = this.relativeGroupTransform; 7720 parentRenderGroup?
vendor: 10,732 bytes, lines 7720-8068
7720.addChild(this); 7721 this._updateIsSimple(); 7722 } 7723 /** @ignore */ 7724 _updateIsSimple() { 7725 this.isSimple = !this.renderGroup && this.effects.length === 0; 7726 } 7727 /** 7728 * Current transform of the object based on world (parent) factors. 7729 * 7730 * This matrix represents the absolute transformation in the scene graph. 7731 * @example 7732 * ```ts 7733 * // Get world position 7734 * const worldPos = container.worldTransform; 7735 * console.log(`World position: (${worldPos.tx}, ${worldPos.ty})`); 7736 * ``` 7737 * @readonly 7738 * @see {@link Container#localTransform} For local space transform 7739 */ 7740 get worldTransform() { 7741 this._worldTransform || (this._worldTransform = new Matrix()); 7742 if (this.renderGroup) { 7743 this._worldTransform.copyFrom(this.renderGroup.worldTransform); 7744 } else if (this.parentRenderGroup) { 7745 this._worldTransform.appendFrom(this.relativeGroupTransform, this.parentRenderGroup.worldTransform); 7746 } 7747 return this._worldTransform; 7748 } 7749 /** 7750 * The position of the container on the x axis relative to the local coordinates of the parent. 7751 * 7752 * An alias to position.x 7753 * @example 7754 * ```ts 7755 * // Basic position 7756 * container.x = 100; 7757 * ``` 7758 */ 7759 get x() { 7760 return this._position.x; 7761 } 7762 set x(value) { 7763 this._position.x = value; 7764 } 7765 /** 7766 * The position of the container on the y axis relative to the local coordinates of the parent. 7767 * 7768 * An alias to position.y 7769 * @example 7770 * ```ts 7771 * // Basic position 7772 * container.y = 200; 7773 * ``` 7774 */ 7775 get y() { 7776 return this._position.y; 7777 } 7778 set y(value) { 7779 this._position.y = value; 7780 } 7781 /** 7782 * The coordinate of the object relative to the local coordinates of the parent. 7783 * @example 7784 * ```ts 7785 * // Basic position setting 7786 * container.position.set(100, 200); 7787 * container.position.set(100); // Sets both x and y to 100 7788 * // Using point data 7789 * container.position = { x: 50, y: 75 }; 7790 * ``` 7791 * @since 4.0.0 7792 */ 7793 get position() { 7794 return this._position; 7795 } 7796 set position(value) { 7797 this._position.copyFrom(value); 7798 } 7799 /** 7800 * The rotation of the object in radians. 7801 * 7802 * > [!NOTE] 'rotation' and 'angle' have the same effect on a display object; 7803 * > rotation is in radians, angle is in degrees. 7804 * @example 7805 * ```ts 7806 * // Basic rotation 7807 * container.rotation = Math.PI / 4; // 45 degrees 7808 * 7809 * // Convert from degrees 7810 * const degrees = 45; 7811 * container.rotation = degrees * Math.PI / 180; 7812 * 7813 * // Rotate around center 7814 * container.pivot.set(container.width / 2, container.height / 2); 7815 * container.rotation = Math.PI; // 180 degrees 7816 * 7817 * // Rotate around center with origin 7818 * container.origin.set(container.width / 2, container.height / 2); 7819 * container.rotation = Math.PI; // 180 degrees 7820 * ``` 7821 */ 7822 get rotation() { 7823 return this._rotation; 7824 } 7825 set rotation(value) { 7826 if (this._rotation !== value) { 7827 this._rotation = value; 7828 this._onUpdate(this._skew); 7829 } 7830 } 7831 /** 7832 * The angle of the object in degrees. 7833 * 7834 * > [!NOTE] 'rotation' and 'angle' have the same effect on a display object; 7835 * > rotation is in radians, angle is in degrees. 7836 * @example 7837 * ```ts 7838 * // Basic angle rotation 7839 * sprite.angle = 45; // 45 degrees 7840 * 7841 * // Rotate around center 7842 * sprite.pivot.set(sprite.width / 2, sprite.height / 2); 7843 * sprite.angle = 180; // Half rotation 7844 * 7845 * // Rotate around center with origin 7846 * sprite.origin.set(sprite.width / 2, sprite.height / 2); 7847 * sprite.angle = 180; // Half rotation 7848 * 7849 * // Reset rotation 7850 * sprite.angle = 0; 7851 * ``` 7852 */ 7853 get angle() { 7854 return this.rotation * RAD_TO_DEG; 7855 } 7856 set angle(value) { 7857 this.rotation = value * DEG_TO_RAD; 7858 } 7859 /** 7860 * The center of rotation, scaling, and skewing for this display object in its local space. 7861 * The `position` is the projection of `pivot` in the parent's local space. 7862 * 7863 * By default, the pivot is the origin (0, 0). 7864 * @example 7865 * ```ts 7866 * // Rotate around center 7867 * container.pivot.set(container.width / 2, container.height / 2); 7868 * container.rotation = Math.PI; // Rotates around center 7869 * ``` 7870 * @since 4.0.0 7871 */ 7872 get pivot() { 7873 if (this._pivot === defaultPivot) { 7874 this._pivot = new ObservablePoint(this, 0, 0); 7875 } 7876 return this._pivot; 7877 } 7878 set pivot(value) { 7879 if (this._pivot === defaultPivot) { 7880 this._pivot = new ObservablePoint(this, 0, 0); 7881 if (this._origin !== defaultOrigin) { 7882 warn(`Setting both a pivot and origin on a Container is not recommended. This can lead to unexpected behavior if not handled carefully.`); 7883 } 7884 } 7885 typeof value === "number" ? this._pivot.set(value) : this._pivot.copyFrom(value); 7886 } 7887 /** 7888 * The skew factor for the object in radians. Skewing is a transformation that distorts 7889 * the object by rotating it differently at each point, creating a non-uniform shape. 7890 * @example 7891 * ```ts 7892 * // Basic skewing 7893 * container.skew.set(0.5, 0); // Skew horizontally 7894 * container.skew.set(0, 0.5); // Skew vertically 7895 * 7896 * // Skew with point data 7897 * container.skew = { x: 0.3, y: 0.3 }; // Diagonal skew 7898 * 7899 * // Reset skew 7900 * container.skew.set(0, 0); 7901 * 7902 * // Animate skew 7903 * app.ticker.add(() => { 7904 * // Create wave effect 7905 * container.skew.x = Math.sin(Date.now() / 1000) * 0.3; 7906 * }); 7907 * 7908 * // Combine with rotation 7909 * container.rotation = Math.PI / 4; // 45 degrees 7910 * container.skew.set(0.2, 0.2); // Skew the rotated object 7911 * ``` 7912 * @since 4.0.0 7913 * @type {ObservablePoint} Point-like object with x/y properties in radians 7914 * @default {x: 0, y: 0} 7915 */ 7916 get skew() { 7917 if (this._skew === defaultSkew) { 7918 this._skew = new ObservablePoint(this, 0, 0); 7919 } 7920 return this._skew; 7921 } 7922 set skew(value) { 7923 if (this._skew === defaultSkew) { 7924 this._skew = new ObservablePoint(this, 0, 0); 7925 } 7926 this._skew.copyFrom(value); 7927 } 7928 /** 7929 * The scale factors of this object along the local coordinate axes. 7930 * 7931 * The default scale is (1, 1). 7932 * @example 7933 * ```ts 7934 * // Basic scaling 7935 * container.scale.set(2, 2); // Scales to double size 7936 * container.scale.set(2); // Scales uniformly to double size 7937 * container.scale = 2; // Scales uniformly to double size 7938 * // Scale to a specific width and height 7939 * container.setSize(200, 100); // Sets width to 200 and height to 100 7940 * ``` 7941 * @since 4.0.0 7942 */ 7943 get scale() { 7944 if (this._scale === defaultScale) { 7945 this._scale = new ObservablePoint(this, 1, 1); 7946 } 7947 return this._scale; 7948 } 7949 set scale(value) { 7950 if (this._scale === defaultScale) { 7951 this._scale = new ObservablePoint(this, 0, 0); 7952 } 7953 if (typeof value === "string") { 7954 value = parseFloat(value); 7955 } 7956 typeof value === "number" ? this._scale.set(value) : this._scale.copyFrom(value); 7957 } 7958 /** 7959 * @experimental 7960 * The origin point around which the container rotates and scales without affecting its position. 7961 * Unlike pivot, changing the origin will not move the container's position. 7962 * @example 7963 * ```ts 7964 * // Rotate around center point 7965 * container.origin.set(container.width / 2, container.height / 2); 7966 * container.rotation = Math.PI; // Rotates around center 7967 * 7968 * // Reset origin 7969 * container.origin.set(0, 0); 7970 * ``` 7971 */ 7972 get origin() { 7973 if (this._origin === defaultOrigin) { 7974 this._origin = new ObservablePoint(this, 0, 0); 7975 } 7976 return this._origin; 7977 } 7978 set origin(value) { 7979 if (this._origin === defaultOrigin) { 7980 this._origin = new ObservablePoint(this, 0, 0); 7981 if (this._pivot !== defaultPivot) { 7982 warn(`Setting both a pivot and origin on a Container is not recommended. This can lead to unexpected behavior if not handled carefully.`); 7983 } 7984 } 7985 typeof value === "number" ? this._origin.set(value) : this._origin.copyFrom(value); 7986 } 7987 /** 7988 * The width of the Container, setting this will actually modify the scale to achieve the value set. 7989 * > [!NOTE] Changing the width will adjust the scale.x property of the container while maintaining its aspect ratio. 7990 * > [!NOTE] If you want to set both width and height at the same time, use {@link Container#setSize} 7991 * as it is more optimized by not recalculating the local bounds twice. 7992 * @example 7993 * ```ts 7994 * // Basic width setting 7995 * container.width = 100; 7996 * // Optimized width setting 7997 * container.setSize(100, 100); 7998 * ``` 7999 */ 8000 get width() { 8001 return Math.abs(this.scale.x * this.getLocalBounds().width); 8002 } 8003 set width(value) { 8004 const localWidth = this.getLocalBounds().width; 8005 this._setWidth(value, localWidth); 8006 } 8007 /** 8008 * The height of the Container, 8009 * > [!NOTE] Changing the height will adjust the scale.y property of the container while maintaining its aspect ratio. 8010 * > [!NOTE] If you want to set both width and height at the same time, use {@link Container#setSize} 8011 * as it is more optimized by not recalculating the local bounds twice. 8012 * @example 8013 * ```ts 8014 * // Basic height setting 8015 * container.height = 200; 8016 * // Optimized height setting 8017 * container.setSize(100, 200); 8018 * ``` 8019 */ 8020 get height() { 8021 return Math.abs(this.scale.y * this.getLocalBounds().height); 8022 } 8023 set height(value) { 8024 const localHeight = this.getLocalBounds().height; 8025 this._setHeight(value, localHeight); 8026 } 8027 /** 8028 * Retrieves the size of the container as a [Size]{@link Size} object. 8029 * 8030 * This is faster than get the width and height separately. 8031 * @example 8032 * ```ts 8033 * // Basic size retrieval 8034 * const size = container.getSize(); 8035 * console.log(`Size: ${size.width}x${size.height}`); 8036 * 8037 * // Reuse existing size object 8038 * const reuseSize = { width: 0, height: 0 }; 8039 * container.getSize(reuseSize); 8040 * ``` 8041 * @param out - Optional object to store the size in. 8042 * @returns The size of the container. 8043 */ 8044 getSize(out) { 8045 if (!out) { 8046 out = {}; 8047 } 8048 const bounds = this.getLocalBounds(); 8049 out.width = Math.abs(this.scale.x * bounds.width); 8050 out.height = Math.abs(this.scale.y * bounds.height); 8051 return out; 8052 } 8053 /** 8054 * Sets the size of the container to the specified width and height. 8055 * This is more efficient than setting width and height separately as it only recalculates bounds once. 8056 * @example 8057 * ```ts 8058 * // Basic size setting 8059 * container.setSize(100, 200); 8060 * 8061 * // Set uniform size 8062 * container.setSize(100); // Sets both width and height to 100 8063 * ``` 8064 * @param value - This can be either a number or a [Size]{@link Size} object. 8065 * @param height - The height to set. Defaults to the value of `width` if not provided. 8066 */ 8067 setSize(value, height) { 8068
8068const size = this.getLocalBounds(); 8069 if (typeof value === "object") { 8070 height = value.height ?? value.width; 8071 value = value.width; 8072 } else { 8073 height ?? (height = value); 8074 } 8075 value !== void 0 && this._setWidth(value, size.width); 8076 height !== void 0 && this._setHeight(height, size.height); 8077 } 8078 /** Called when the skew or the rotation changes. */ 8079 _updateSkew() { 8080 const rotation = this._rotation; 8081 const skew = this._skew; 8082 this._cx = Math.cos(rotation + skew._y); 8083 this._sx = Math.sin(rotation + skew._y); 8084 this._cy = -Math.sin(rotation - skew._x); 8085 this._sy = Math.cos(rotation - skew._x); 8086 } 8087 /** 8088 * Updates the transform properties of the container. 8089 * Allows partial updates of transform properties for optimized manipulation. 8090 * @example 8091 * ```ts 8092 * // Basic transform update 8093 * container.updateTransform({ 8094 * x: 100, 8095 * y: 200, 8096 * rotation: Math.PI / 4 8097 * }); 8098 * 8099 * // Scale and rotate around center 8100 * sprite.updateTransform({ 8101 * pivotX: sprite.width / 2, 8102 * pivotY: sprite.height / 2, 8103 * scaleX: 2, 8104 * scaleY: 2, 8105 * rotation: Math.PI 8106 * }); 8107 * 8108 * // Update position only 8109 * button.updateTransform({ 8110 * x: button.x + 10, // Move right 8111 * y: button.y // Keep same y 8112 * }); 8113 * ``` 8114 * @param opts - Transform options to update 8115 * @param opts.x - The x position 8116 * @param opts.y - The y position 8117 * @param opts.scaleX - The x-axis scale factor 8118 * @param opts.scaleY - The y-axis scale factor 8119 * @param opts.rotation - The rotation in radians 8120 * @param opts.skewX - The x-axis skew factor 8121 * @param opts.skewY - The y-axis skew factor 8122 * @param opts.pivotX - The x-axis pivot point 8123 * @param opts.pivotY - The y-axis pivot point 8124 * @returns This container, for chaining 8125 * @see {@link Container#setFromMatrix} For matrix-based transforms 8126 * @see {@link Container#position} For direct position access 8127 */ 8128 updateTransform(opts) { 8129 this.position.set( 8130 typeof opts.x === "number" ? opts.x : this.position.x, 8131 typeof opts.y === "number" ? opts.y : this.position.y 8132 ); 8133 this.scale.set( 8134 typeof opts.scaleX === "number" ? opts.scaleX || 1 : this.scale.x, 8135 typeof opts.scaleY === "number" ? opts.scaleY || 1 : this.scale.y 8136 ); 8137 this.rotation = typeof opts.rotation === "number" ? opts.rotation : this.rotation; 8138 this.skew.set( 8139 typeof opts.skewX === "number" ? opts.skewX : this.skew.x, 8140 typeof opts.skewY === "number" ? opts.skewY : this.skew.y 8141 ); 8142 this.pivot.set( 8143 typeof opts.pivotX === "number" ? opts.pivotX : this.pivot.x, 8144 typeof opts.pivotY === "number" ? opts.pivotY : this.pivot.y 8145 ); 8146 this.origin.set( 8147 typeof opts.originX === "number" ? opts.originX : this.origin.x, 8148 typeof opts.originY === "number" ? opts.originY : this.origin.y 8149 ); 8150 return this; 8151 } 8152 /** 8153 * Updates the local transform properties by decomposing the given matrix. 8154 * Extracts position, scale, rotation, and skew from a transformation matrix. 8155 * @example 8156 * ```ts 8157 * // Basic matrix transform 8158 * const matrix = new Matrix() 8159 * .translate(100, 100) 8160 * .rotate(Math.PI / 4) 8161 * .scale(2, 2); 8162 * 8163 * container.setFromMatrix(matrix); 8164 * 8165 * // Copy transform from another container 8166 * const source = new Container(); 8167 * source.position.set(100, 100); 8168 * source.rotation = Math.PI / 2; 8169 * 8170 * target.setFromMatrix(source.localTransform); 8171 * 8172 * // Reset transform 8173 * container.setFromMatrix(Matrix.IDENTITY); 8174 * ``` 8175 * @param matrix - The matrix to use for updating the transform 8176 * @see {@link Container#updateTransform} For property-based updates 8177 * @see {@link Matrix#decompose} For matrix decomposition details 8178 */ 8179 setFromMatrix(matrix) { 8180 matrix.decompose(this); 8181 } 8182 /** Updates the local transform. */ 8183 updateLocalTransform() { 8184 const localTransformChangeId = this._didContainerChangeTick; 8185 if (this._didLocalTransformChangeId === localTransformChangeId) 8186 return; 8187 this._didLocalTransformChangeId = localTransformChangeId; 8188 const lt = this.localTransform; 8189 const scale = this._scale; 8190 const pivot = this._pivot; 8191 const origin = this._origin; 8192 const position = this._position; 8193 const sx = scale._x; 8194 const sy = scale._y; 8195 const px = pivot._x; 8196 const py = pivot._y;
8197 const ox = -origin._x; 8198 const oy = -origin._y; 8199 lt.a = this._cx * sx; 8200 lt.b = this._sx * sx; 8201 lt.c = this._cy * sy; 8202 lt.d = this._sy * sy; 8203 lt.tx = position._x - (px * lt.a + py * lt.c) + (ox * lt.a + oy * lt.c) - ox; 8204 lt.ty = position._y - (px * lt.b + py * lt.d) + (ox * lt.b + oy * lt.d) - oy; 8205 } 8206 // / ///// color related stuff 8207 set alpha(value) { 8208 if (value === this.localAlpha) 8209 return; 8210 this.localAlpha = value; 8211 this._updateFlags |= UPDATE_COLOR; 8212 this._onUpdate(); 8213 } 8214 /** 8215 * The opacity of the object relative to its parent's opacity. 8216 * Value ranges from 0 (fully transparent) to 1 (fully opaque). 8217 * @example 8218 * ```ts 8219 * // Basic transparency 8220 * sprite.alpha = 0.5; // 50% opacity 8221 * 8222 * // Inherited opacity 8223 * container.alpha = 0.5; 8224 * const child = new Sprite(texture); 8225 * child.alpha = 0.5; 8226 * container.addChild(child); 8227 * // child's effective opacity is 0.25 (0.5 * 0.5) 8228 * ``` 8229 * @default 1 8230 * @see {@link Container#visible} For toggling visibility 8231 * @see {@link Container#renderable} For render control 8232 */ 8233 get alpha() { 8234 return this.localAlpha; 8235 } 8236 set tint(value) { 8237 const tempColor = Color.shared.setValue(value ?? 16777215); 8238 const bgr = tempColor.toBgrNumber(); 8239 if (bgr === this.localColor) 8240 return; 8241 this.localColor = bgr; 8242 this._updateFlags |= UPDATE_COLOR; 8243 this._onUpdate(); 8244 } 8245 /** 8246 * The tint applied to the sprite. 8247 * 8248 * This can be any valid {@link ColorSource}. 8249 * @example 8250 * ```ts 8251 * // Basic color tinting 8252 * container.tint = 0xff0000; // Red tint 8253 * container.tint = 'red'; // Same as above 8254 * container.tint = '#00ff00'; // Green 8255 * container.tint = 'rgb(0,0,255)'; // Blue 8256 * 8257 * // Remove tint 8258 * container.tint = 0xffffff; // White = no tint 8259 * container.tint = null; // Also removes tint 8260 * ``` 8261 * @default 0xFFFFFF 8262 * @see {@link Container#alpha} For transparency 8263 * @see {@link Container#visible} For visibility control 8264 */ 8265 get tint() { 8266 return bgr2rgb(this.localColor); 8267 } 8268 // / //////////////// blend related stuff 8269 set blendMode(value) {
vendor: 4,385 bytes, lines 8270-8410
8270 if (this.localBlendMode === value) 8271 return; 8272 if (this.parentRenderGroup) { 8273 this.parentRenderGroup.structureDidChange = true; 8274 } 8275 this._updateFlags |= UPDATE_BLEND; 8276 this.localBlendMode = value; 8277 this._onUpdate(); 8278 } 8279 /** 8280 * The blend mode to be applied to the sprite. Controls how pixels are blended when rendering. 8281 * 8282 * Setting to 'normal' will reset to default blending. 8283 * > [!NOTE] More blend modes are available after importing the `pixi.js/advanced-blend-modes` sub-export. 8284 * @example 8285 * ```ts 8286 * // Basic blend modes 8287 * sprite.blendMode = 'add'; // Additive blending 8288 * sprite.blendMode = 'multiply'; // Multiply colors 8289 * sprite.blendMode = 'screen'; // Screen blend 8290 * 8291 * // Reset blend mode 8292 * sprite.blendMode = 'normal'; // Normal blending 8293 * ``` 8294 * @default 'normal' 8295 * @see {@link Container#alpha} For transparency 8296 * @see {@link Container#tint} For color adjustments 8297 */ 8298 get blendMode() { 8299 return this.localBlendMode; 8300 } 8301 // / ///////// VISIBILITY / RENDERABLE ///////////////// 8302 /** 8303 * The visibility of the object. If false the object will not be drawn, 8304 * and the transform will not be updated. 8305 * @example 8306 * ```ts 8307 * // Basic visibility toggle 8308 * sprite.visible = false; // Hide sprite 8309 * sprite.visible = true; // Show sprite 8310 * ``` 8311 * @default true 8312 * @see {@link Container#renderable} For render-only control 8313 * @see {@link Container#alpha} For transparency 8314 */ 8315 get visible() { 8316 return !!(this.localDisplayStatus & 2); 8317 } 8318 set visible(value) { 8319 const valueNumber = value ? 2 : 0; 8320 if ((this.localDisplayStatus & 2) === valueNumber) 8321 return; 8322 if (this.parentRenderGroup) { 8323 this.parentRenderGroup.structureDidChange = true; 8324 } 8325 this._updateFlags |= UPDATE_VISIBLE; 8326 this.localDisplayStatus ^= 2; 8327 this._onUpdate(); 8328 } 8329 /** @ignore */ 8330 get culled() { 8331 return !(this.localDisplayStatus & 4); 8332 } 8333 /** @ignore */ 8334 set culled(value) { 8335 const valueNumber = value ? 0 : 4; 8336 if ((this.localDisplayStatus & 4) === valueNumber) 8337 return; 8338 if (this.parentRenderGroup) { 8339 this.parentRenderGroup.structureDidChange = true; 8340 } 8341 this._updateFlags |= UPDATE_VISIBLE; 8342 this.localDisplayStatus ^= 4; 8343 this._onUpdate(); 8344 } 8345 /** 8346 * Controls whether this object can be rendered. If false the object will not be drawn, 8347 * but the transform will still be updated. This is different from visible, which skips 8348 * transform updates. 8349 * @example 8350 * ```ts 8351 * // Basic render control 8352 * sprite.renderable = false; // Skip rendering 8353 * sprite.renderable = true; // Enable rendering 8354 * ``` 8355 * @default true 8356 * @see {@link Container#visible} For skipping transform updates 8357 * @see {@link Container#alpha} For transparency 8358 */ 8359 get renderable() { 8360 return !!(this.localDisplayStatus & 1); 8361 } 8362 set renderable(value) { 8363 const valueNumber = value ? 1 : 0; 8364 if ((this.localDisplayStatus & 1) === valueNumber) 8365 return; 8366 this._updateFlags |= UPDATE_VISIBLE; 8367 this.localDisplayStatus ^= 1; 8368 if (this.parentRenderGroup) { 8369 this.parentRenderGroup.structureDidChange = true; 8370 } 8371 this._onUpdate(); 8372 } 8373 /** 8374 * Whether or not the object should be rendered. 8375 * @advanced 8376 */ 8377 get isRenderable() { 8378 return this.localDisplayStatus === 7 && this.groupAlpha > 0; 8379 } 8380 /** 8381 * Removes all internal references and listeners as well as removes children from the display list. 8382 * Do not use a Container after calling `destroy`. 8383 * @param options - Options parameter. A boolean will act as if all options 8384 * have been set to that value 8385 * @example 8386 * ```ts 8387 * container.destroy(); 8388 * container.destroy(true); 8389 * container.destroy({ children: true }); 8390 * container.destroy({ children: true, texture: true, textureSource: true }); 8391 * ``` 8392 */ 8393 destroy(options = false) { 8394 if (this.destroyed) 8395 return; 8396 this.destroyed = true; 8397 let oldChildren; 8398 if (this.children.length) { 8399 oldChildren = this.removeChildren(0, this.children.length); 8400 } 8401 this.removeFromParent(); 8402 this.parent = null; 8403 this._maskEffect = null; 8404 this._filterEffect = null; 8405 this.effects = null; 8406 this._position = null; 8407 this._scale = null; 8408 this._pivot = null; 8409 this._origin = null; 8410 this._skew = null;
8411 this.emit("destroyed", this); 8412 this.removeAllListeners(); 8413 const destroyChildren = typeof options === "boolean" ? options : options?.children; 8414 if (destroyChildren && oldChildren) { 8415 for (let i = 0; i < oldChildren.length; ++i) { 8416 oldChildren[i].destroy(options); 8417 } 8418 } 8419 this.renderGroup?.destroy(); 8420 this.renderGroup = null; 8421 } 8422} 8423extensions.mixin( 8424 Container, 8425 childrenHelperMixin, 8426 getFastGlobalBoundsMixin, 8427 toLocalGlobalMixin, 8428 onRenderMixin, 8429 measureMixin, 8430 effectsMixin, 8431 findMixin, 8432 sortMixin, 8433 cullingMixin, 8434 cacheAsTextureMixin, 8435 getGlobalMixin, 8436 collectRenderablesMixin 8437); 8438 8439class ViewContainer extends Container { 8440 constructor(options) { 8441 super(options); 8442 /** @internal */ 8443 this.canBundle = true; 8444 /** @internal */ 8445 this.allowChildren = false; 8446 /** @internal */ 8447 this._roundPixels = 0; 8448 /** @internal */ 8449 this._lastUsed = -1; 8450 /** @internal */ 8451 this._gpuData = /* @__PURE__ */ Object.create(null); 8452 /** If set to true, the resource will be garbage collected automatically when it is not used. */ 8453 this.autoGarbageCollect = true; 8454 /** @internal */ 8455 this._gcLastUsed = -1; 8456 this._bounds = new Bounds(0, 1, 0, 0); 8457 this._boundsDirty = true; 8458 this.autoGarbageCollect = options.autoGarbageCollect ?? true; 8459 } 8460 /** 8461 * The local bounds of the view in its own coordinate space. 8462 * Bounds are automatically updated when the view's content changes. 8463 * @example 8464 * ```ts 8465 * // Get bounds dimensions 8466 * const bounds = view.bounds; 8467 * console.log(`Width: ${bounds.maxX - bounds.minX}`); 8468 * console.log(`Height: ${bounds.maxY - bounds.minY}`); 8469 * ``` 8470 * @returns The rectangular bounds of the view 8471 * @see {@link Bounds} For bounds operations 8472 */ 8473 get bounds() { 8474 if (!this._boundsDirty) 8475 return this._bounds; 8476 this.updateBounds(); 8477 this._boundsDirty = false; 8478 return this._bounds; 8479 } 8480 /** 8481 * Whether or not to round the x/y position of the sprite. 8482 * @example 8483 * ```ts 8484 * // Enable pixel rounding for crisp rendering 8485 * view.roundPixels = true; 8486 * ``` 8487 * @default false 8488 */ 8489 get roundPixels() { 8490 return !!this._roundPixels; 8491 } 8492 set roundPixels(value) { 8493 this._roundPixels = value ? 1 : 0; 8494 } 8495 /** 8496 * Checks if the object contains the given point in local coordinates. 8497 * Uses the view's bounds for hit testing. 8498 * @example 8499 * ```ts 8500 * // Basic point check 8501 * const localPoint = { x: 50, y: 25 }; 8502 * const contains = view.containsPoint(localPoint); 8503 * console.log('Point is inside:', contains); 8504 * ``` 8505 * @param point - The point to check in local coordinates 8506 * @returns True if the point is within the view's bounds 8507 * @see {@link ViewContainer#bounds} For the bounds used in hit testing 8508 * @see {@link Container#toLocal} For converting global coordinates to local 8509 */ 8510 containsPoint(point) { 8511 const bounds = this.bounds; 8512 const { x, y } = point; 8513 return x >= bounds.minX && x <= bounds.maxX && y >= bounds.minY && y <= bounds.maxY; 8514 } 8515 /** @private */ 8516 onViewUpdate() { 8517 this._didViewChangeTick++; 8518 this._boundsDirty = true; 8519 if (this.didViewUpdate) 8520 return; 8521 this.didViewUpdate = true; 8522 const renderGroup = this.renderGroup || this.parentRenderGroup; 8523 if (renderGroup) { 8524 renderGroup.onChildViewUpdate(this); 8525 } 8526 } 8527 /** Unloads the GPU data from the view. */ 8528 unload() { 8529 this.emit("unload", this); 8530 for (const key in this._gpuData) { 8531 this._gpuData[key]?.destroy(); 8532 } 8533 this._gpuData = /* @__PURE__ */ Object.create(null); 8534 this.onViewUpdate(); 8535 } 8536 destroy(options) { 8537 this.unload(); 8538 super.destroy(options); 8539 this._bounds = null; 8540 } 8541 /** 8542 * Collects renderables for the view container. 8543 * @param instructionSet - The instruction set to collect renderables for. 8544 * @param renderer - The renderer to collect renderables for. 8545 * @param currentLayer - The current render layer. 8546 * @internal 8547 */ 8548 collectRenderablesSimple(instructionSet, renderer, currentLayer) { 8549 const { renderPipes } = renderer; 8550 renderPipes.blendMode.pushBlendMode(this, this.groupBlendMode, instructionSet); 8551 const rp = renderPipes; 8552 rp[this.renderPipeId].addRenderable(this, instructionSet); 8553 this.didViewUpdate = false;
8554 const children = this.children; 8555 const length = children.length; 8556 for (let i = 0; i < length; i++) { 8557 children[i].collectRenderables(instructionSet, renderer, currentLayer); 8558 } 8559 renderPipes.blendMode.popBlendMode(instructionSet); 8560 } 8561} 8562 8563class Sprite extends ViewContainer { 8564 /** 8565 * @param options - The options for creating the sprite. 8566 */ 8567 constructor(options = Texture.EMPTY) { 8568 if (options instanceof Texture) { 8569 options = { texture: options }; 8570 } 8571 const { texture = Texture.EMPTY, anchor, roundPixels, width, height, ...rest } = options; 8572 super({ 8573 label: "Sprite", 8574 ...rest 8575 }); 8576 /** @internal */ 8577 this.renderPipeId = "sprite"; 8578 /** @internal */ 8579 this.batched = true; 8580 this._visualBounds = { minX: 0, maxX: 1, minY: 0, maxY: 0 }; 8581 this._anchor = new ObservablePoint( 8582 { 8583 _onUpdate: () => { 8584 this.onViewUpdate(); 8585 } 8586 } 8587 ); 8588 if (anchor) { 8589 this.anchor = anchor; 8590 } else if (texture.defaultAnchor) { 8591 this.anchor = texture.defaultAnchor; 8592 } 8593 this.texture = texture; 8594 this.allowChildren = false; 8595 this.roundPixels = roundPixels ?? false; 8596 if (width !== void 0) 8597 this.width = width; 8598 if (height !== void 0) 8599 this.height = height; 8600 } 8601 /** 8602 * Creates a new sprite based on a source texture, image, video, or canvas element. 8603 * This is a convenience method that automatically creates and manages textures. 8604 * @example 8605 * ```ts 8606 * // Create from path or URL 8607 * const sprite = Sprite.from('assets/image.png'); 8608 * 8609 * // Create from existing texture 8610 * const sprite = Sprite.from(texture); 8611 * 8612 * // Create from canvas 8613 * const canvas = document.createElement('canvas'); 8614 * const sprite = Sprite.from(canvas, true); // Skip caching new texture 8615 * ``` 8616 * @param source - The source to create the sprite from. Can be a path to an image, a texture, 8617 * or any valid texture source (canvas, video, etc.) 8618 * @param skipCache - Whether to skip adding to the texture cache when creating a new texture 8619 * @returns A new sprite based on the source 8620 * @see {@link Texture.from} For texture creation details 8621 * @see {@link Assets} For asset loading and management 8622 */ 8623 static from(source, skipCache = false) { 8624 if (source instanceof Texture) { 8625 return new Sprite(source); 8626 } 8627 return new Sprite(Texture.from(source, skipCache)); 8628 } 8629 set texture(value) { 8630 value || (value = Texture.EMPTY); 8631 const currentTexture = this._texture; 8632 if (currentTexture === value) 8633 return; 8634 if (currentTexture && currentTexture.dynamic) 8635 currentTexture.off("update", this.onViewUpdate, this); 8636 if (value.dynamic) 8637 value.on("update", this.onViewUpdate, this); 8638 this._texture = value; 8639 if (this._width) { 8640 this._setWidth(this._width, this._texture.orig.width); 8641 } 8642 if (this._height) { 8643 this._setHeight(this._height, this._texture.orig.height); 8644 } 8645 this.onViewUpdate(); 8646 } 8647 /** 8648 * The texture that is displayed by the sprite. When changed, automatically updates 8649 * the sprite dimensions and manages texture event listeners. 8650 * @example 8651 * ```ts 8652 * // Create sprite with texture 8653 * const sprite = new Sprite({ 8654 * texture: Texture.from('sprite.png') 8655 * }); 8656 * 8657 * // Update texture 8658 * sprite.texture = Texture.from('newSprite.png'); 8659 * 8660 * // Use texture from spritesheet 8661 * const sheet = await Assets.load('spritesheet.json'); 8662 * sprite.texture = sheet.textures['frame1.png']; 8663 * 8664 * // Reset to empty texture 8665 * sprite.texture = Texture.EMPTY; 8666 * ``` 8667 * @see {@link Texture} For texture creation and management 8668 * @see {@link Assets} For asset loading 8669 */ 8670 get texture() { 8671 return this._texture; 8672 } 8673 /** 8674 * The bounds of the sprite, taking into account the texture's trim area. 8675 * @example 8676 * ```ts 8677 * const texture = new Texture({ 8678 * source: new TextureSource({ width: 300, height: 300 }), 8679 * frame: new Rectangle(196, 66, 58, 56), 8680 * trim: new Rectangle(4, 4, 58, 56), 8681 * orig: new Rectangle(0, 0, 64, 64), 8682 * rotate: 2, 8683 * }); 8684 * const sprite = new Sprite(texture); 8685 * const visualBounds = sprite.visualBounds; 8686 * // console.log(visualBounds); // { minX: -4, maxX: 62, minY: -4, maxY: 60 } 8687 */ 8688 get visualBounds() { 8689 updateQuadBounds(this._visualBounds, this._anchor, this._texture); 8690 return this._visualBounds; 8691 } 8692 /** 8693 * @deprecated 8694 * @ignore 8695 */ 8696 get sourceBounds() { 8697 deprecation("8.6.1", "Sprite.sourceBounds is deprecated, use visualBounds instead."); 8698 return this.visualBounds; 8699 } 8700 /** @private */ 8701 updateBounds() { 8702 const anchor = this._anchor; 8703 const texture = this._texture; 8704 const bounds = this._bounds; 8705 const { width, height } = texture.orig; 8706 bounds.minX = -anchor._x * width; 8707 bounds.maxX = bounds.minX + width; 8708 bounds.minY = -anchor._y * height; 8709 bounds.maxY = bounds.minY + height; 8710 } 8711 /** 8712 * Destroys this sprite renderable and optionally its texture. 8713 * @param options - Options parameter. A boolean will act as if all options 8714 * have been set to that value 8715 * @example 8716 * sprite.destroy(); 8717 * sprite.destroy(true); 8718 * sprite.destroy({ texture: true, textureSource: true }); 8719 */ 8720 destroy(options = false) { 8721 super.destroy(options); 8722 const destroyTexture = typeof options === "boolean" ? options : options?.texture; 8723 if (destroyTexture) { 8724 const destroyTextureSource = typeof options === "boolean" ? options : options?
vendor: 5,378 bytes, lines 8724-8882
8724.textureSource; 8725 this._texture.destroy(destroyTextureSource); 8726 } 8727 this._texture = null; 8728 this._visualBounds = null; 8729 this._bounds = null; 8730 this._anchor = null; 8731 } 8732 /** 8733 * The anchor sets the origin point of the sprite. The default value is taken from the {@link Texture} 8734 * and passed to the constructor. 8735 * 8736 * - The default is `(0,0)`, this means the sprite's origin is the top left. 8737 * - Setting the anchor to `(0.5,0.5)` means the sprite's origin is centered. 8738 * - Setting the anchor to `(1,1)` would mean the sprite's origin point will be the bottom right corner. 8739 * 8740 * If you pass only single parameter, it will set both x and y to the same value as shown in the example below. 8741 * @example 8742 * ```ts 8743 * // Center the anchor point 8744 * sprite.anchor = 0.5; // Sets both x and y to 0.5 8745 * sprite.position.set(400, 300); // Sprite will be centered at this position 8746 * 8747 * // Set specific x/y anchor points 8748 * sprite.anchor = { 8749 * x: 1, // Right edge 8750 * y: 0 // Top edge 8751 * }; 8752 * 8753 * // Using individual coordinates 8754 * sprite.anchor.set(0.5, 1); // Center-bottom 8755 * 8756 * // For rotation around center 8757 * sprite.anchor.set(0.5); 8758 * sprite.rotation = Math.PI / 4; // 45 degrees around center 8759 * 8760 * // For scaling from center 8761 * sprite.anchor.set(0.5); 8762 * sprite.scale.set(2); // Scales from center point 8763 * ``` 8764 */ 8765 get anchor() { 8766 return this._anchor; 8767 } 8768 set anchor(value) { 8769 typeof value === "number" ? this._anchor.set(value) : this._anchor.copyFrom(value); 8770 } 8771 /** 8772 * The width of the sprite, setting this will actually modify the scale to achieve the value set. 8773 * @example 8774 * ```ts 8775 * // Set width directly 8776 * sprite.width = 200; 8777 * console.log(sprite.scale.x); // Scale adjusted to match width 8778 * 8779 * // Set width while preserving aspect ratio 8780 * const ratio = sprite.height / sprite.width; 8781 * sprite.width = 300; 8782 * sprite.height = 300 * ratio; 8783 * 8784 * // For better performance when setting both width and height 8785 * sprite.setSize(300, 400); // Avoids recalculating bounds twice 8786 * 8787 * // Reset to original texture size 8788 * sprite.width = sprite.texture.orig.width; 8789 * ``` 8790 */ 8791 get width() { 8792 return Math.abs(this.scale.x) * this._texture.orig.width; 8793 } 8794 set width(value) { 8795 this._setWidth(value, this._texture.orig.width); 8796 this._width = value; 8797 } 8798 /** 8799 * The height of the sprite, setting this will actually modify the scale to achieve the value set. 8800 * @example 8801 * ```ts 8802 * // Set height directly 8803 * sprite.height = 150; 8804 * console.log(sprite.scale.y); // Scale adjusted to match height 8805 * 8806 * // Set height while preserving aspect ratio 8807 * const ratio = sprite.width / sprite.height; 8808 * sprite.height = 200; 8809 * sprite.width = 200 * ratio; 8810 * 8811 * // For better performance when setting both width and height 8812 * sprite.setSize(300, 400); // Avoids recalculating bounds twice 8813 * 8814 * // Reset to original texture size 8815 * sprite.height = sprite.texture.orig.height; 8816 * ``` 8817 */ 8818 get height() { 8819 return Math.abs(this.scale.y) * this._texture.orig.height; 8820 } 8821 set height(value) { 8822 this._setHeight(value, this._texture.orig.height); 8823 this._height = value; 8824 } 8825 /** 8826 * Retrieves the size of the Sprite as a [Size]{@link Size} object based on the texture dimensions and scale. 8827 * This is faster than getting width and height separately as it only calculates the bounds once. 8828 * @example 8829 * ```ts 8830 * // Basic size retrieval 8831 * const sprite = new Sprite(Texture.from('sprite.png')); 8832 * const size = sprite.getSize(); 8833 * console.log(`Size: ${size.width}x${size.height}`); 8834 * 8835 * // Reuse existing size object 8836 * const reuseSize = { width: 0, height: 0 }; 8837 * sprite.getSize(reuseSize); 8838 * ``` 8839 * @param out - Optional object to store the size in, to avoid allocating a new object 8840 * @returns The size of the Sprite 8841 * @see {@link Sprite#width} For getting just the width 8842 * @see {@link Sprite#height} For getting just the height 8843 * @see {@link Sprite#setSize} For setting both width and height 8844 */ 8845 getSize(out) { 8846 out || (out = {}); 8847 out.width = Math.abs(this.scale.x) * this._texture.orig.width; 8848 out.height = Math.abs(this.scale.y) * this._texture.orig.height; 8849 return out; 8850 } 8851 /** 8852 * Sets the size of the Sprite to the specified width and height. 8853 * This is faster than setting width and height separately as it only recalculates bounds once. 8854 * @example 8855 * ```ts 8856 * // Basic size setting 8857 * const sprite = new Sprite(Texture.from('sprite.png')); 8858 * sprite.setSize(100, 200); // Width: 100, Height: 200 8859 * 8860 * // Set uniform size 8861 * sprite.setSize(100); // Sets both width and height to 100 8862 * 8863 * // Set size with object 8864 * sprite.setSize({ 8865 * width: 200, 8866 * height: 300 8867 * }); 8868 * 8869 * // Reset to texture size 8870 * sprite.setSize( 8871 * sprite.texture.orig.width, 8872 * sprite.texture.orig.height 8873 * ); 8874 * ``` 8875 * @param value - This can be either a number or a {@link Size} object 8876 * @param height - The height to set. Defaults to the value of `width` if not provided 8877 * @see {@link Sprite#width} For setting width only 8878 * @see {@link Sprite#height} For setting height only 8879 * @see {@link Sprite#texture} For the source dimensions 8880 */ 8881 setSize(value, height) { 8882
8882if (typeof value === "object") { 8883 height = value.height ?? value.width; 8884 value = value.width; 8885 } else { 8886 height ?? (height = value); 8887 } 8888 value !== void 0 && this._setWidth(value, this._texture.orig.width); 8889 height !== void 0 && this._setHeight(height, this._texture.orig.height); 8890 } 8891} 8892 8893const tempBounds = new Bounds(); 8894function addMaskBounds(mask, bounds, skipUpdateTransform) { 8895 const boundsToMask = tempBounds; 8896 mask.measurable = true; 8897 getGlobalBounds(mask, skipUpdateTransform, boundsToMask); 8898 bounds.addBoundsMask(boundsToMask); 8899 mask.measurable = false; 8900} 8901 8902function addMaskLocalBounds(mask, bounds, localRoot) { 8903 const boundsToMask = boundsPool.get(); 8904 mask.measurable = true; 8905 const tempMatrix = matrixPool.get().identity(); 8906 const relativeMask = getMatrixRelativeToParent(mask, localRoot, tempMatrix); 8907 getLocalBounds(mask, boundsToMask, relativeMask); 8908 mask.measurable = false; 8909 bounds.addBoundsMask(boundsToMask); 8910 matrixPool.return(tempMatrix); 8911 boundsPool.return(boundsToMask); 8912}
8913function getMatrixRelativeToParent(target, root, matrix) { 8914 if (!target) { 8915 warn("Mask bounds, renderable is not inside the root container"); 8916 return matrix; 8917 } 8918 if (target !== root) { 8919 getMatrixRelativeToParent(target.parent, root, matrix); 8920 target.updateLocalTransform(); 8921 matrix.append(target.localTransform); 8922 } 8923 return matrix; 8924} 8925 8926class AlphaMask { 8927 constructor(options) { 8928 this.priority = 0; 8929 this.inverse = false; 8930 this.pipe = "alphaMask"; 8931 if (options?.mask) { 8932 this.init(options.mask); 8933 } 8934 } 8935 init(mask) { 8936 this.mask = mask; 8937 this.renderMaskToTexture = !(mask instanceof Sprite); 8938 this.mask.renderable = this.renderMaskToTexture; 8939 this.mask.includeInBuild = !this.renderMaskToTexture; 8940 this.mask.measurable = false; 8941 } 8942 reset() { 8943 if (this.mask === null) 8944 return; 8945 this.mask.measurable = true; 8946 this.mask = null; 8947 } 8948 addBounds(bounds, skipUpdateTransform) { 8949 if (!this.inverse) { 8950 addMaskBounds(this.mask, bounds, skipUpdateTransform); 8951 } 8952 } 8953 addLocalBounds(bounds, localRoot) { 8954 addMaskLocalBounds(this.mask, bounds, localRoot); 8955 } 8956 containsPoint(point, hitTestFn) { 8957 const mask = this.mask; 8958 return hitTestFn(mask, point); 8959 } 8960 destroy() { 8961 this.reset(); 8962 } 8963 static test(mask) { 8964 return mask instanceof Sprite; 8965 } 8966} 8967AlphaMask.extension = ExtensionType.MaskEffect; 8968 8969class ColorMask { 8970 constructor(options) { 8971 this.priority = 0; 8972 this.pipe = "colorMask"; 8973 if (options?.mask) { 8974 this.init(options.mask); 8975 } 8976 } 8977 init(mask) { 8978 this.mask = mask; 8979 } 8980 destroy() { 8981 } 8982 static test(mask) { 8983 return typeof mask === "number"; 8984 } 8985} 8986ColorMask.extension = ExtensionType.MaskEffect; 8987 8988class StencilMask { 8989 constructor(options) { 8990 this.priority = 0; 8991 this.pipe = "stencilMask"; 8992 if (options?.mask) { 8993 this.init(options.mask); 8994 } 8995 } 8996 init(mask) { 8997 this.mask = mask; 8998 this.mask.includeInBuild = false; 8999 this.mask.measurable = false; 9000 } 9001 reset() { 9002 if (this.mask === null) 9003 return; 9004 this.mask.measurable = true; 9005 this.mask.includeInBuild = true; 9006 this.mask = null; 9007 } 9008 addBounds(bounds, skipUpdateTransform) { 9009 addMaskBounds(this.mask, bounds, skipUpdateTransform); 9010 } 9011 addLocalBounds(bounds, localRoot) { 9012 addMaskLocalBounds(this.mask, bounds, localRoot); 9013 } 9014 containsPoint(point, hitTestFn) { 9015 const mask = this.mask; 9016 return hitTestFn(mask, point); 9017 } 9018 destroy() { 9019 this.reset(); 9020 } 9021 static test(mask) { 9022 return mask instanceof Container; 9023 } 9024} 9025StencilMask.extension = ExtensionType.MaskEffect; 9026 9027const BrowserAdapter = { 9028 createCanvas: (width, height) => { 9029 const canvas = document.createElement("canvas"); 9030 canvas.width = width; 9031 canvas.height = height; 9032 return canvas; 9033 }, 9034 createImage: () => new Image(), 9035 getCanvasRenderingContext2D: () => CanvasRenderingContext2D, 9036 getWebGLRenderingContext: () => WebGLRenderingContext, 9037 getNavigator: () => navigator, 9038 getBaseUrl: () => document.baseURI ?? window.location.href, 9039 getFontFaceSet: () => document.fonts, 9040 fetch: (url, options) => fetch(url, options), 9041 parseXML: (xml) => { 9042 const parser = new DOMParser(); 9043 return parser.parseFromString(xml, "text/xml"); 9044 } 9045}; 9046 9047let currentAdapter = BrowserAdapter; 9048const DOMAdapter = { 9049 /** 9050 * Returns the current adapter. 9051 * @returns {environment.Adapter} The current adapter. 9052 */ 9053 get() { 9054 return currentAdapter; 9055 }, 9056 /** 9057 * Sets the current adapter. 9058 * @param adapter - The new adapter. 9059 */ 9060 set(adapter) { 9061 currentAdapter = adapter; 9062 } 9063}; 9064 9065class CanvasSource extends TextureSource { 9066 constructor(options) { 9067 if (!options.resource) { 9068 options.resource = DOMAdapter.get().createCanvas(); 9069 } 9070 if (!options.width) { 9071 options.width = options.resource.width; 9072 if (!options.autoDensity) { 9073 options.width /= options.resolution; 9074 } 9075 } 9076 if (!options.height) { 9077 options.height = options.resource.height; 9078 if (!options.autoDensity) { 9079 options.height /= options.resolution; 9080 } 9081 } 9082 super(options); 9083 this.uploadMethodId = "image"; 9084 this.autoDensity = options.autoDensity; 9085 this.resizeCanvas(); 9086 this.transparent = !!options.transparent; 9087 } 9088 resizeCanvas() { 9089 if (this.autoDensity && "style" in this.resource) { 9090 this.resource.style.width = `${this.width}px`; 9091 this.resource.style.height = `${this.height}px`; 9092 } 9093 if (this.resource.width !== this.pixelWidth || this.resource.height !== this.pixelHeight) { 9094 this.resource.width = this.pixelWidth; 9095 this.resource.height = this.pixelHeight; 9096 } 9097 } 9098 resize(width = this.width, height = this.height, resolution = this._resolution) { 9099 const didResize = super.resize(width, height, resolution); 9100 if (didResize) { 9101 this.resizeCanvas(); 9102 } 9103 return didResize; 9104 } 9105 static test(resource) { 9106 return globalThis.HTMLCanvasElement && resource instanceof HTMLCanvasElement || globalThis.OffscreenCanvas && resource instanceof OffscreenCanvas; 9107 } 9108 /** 9109 * Returns the 2D rendering context for the canvas. 9110 * Caches the context after creating it. 9111 * @returns The 2D rendering context of the canvas. 9112 */ 9113 get context2D() { 9114 return this._context2D || (this._context2D = this.resource.getContext("2d")); 9115 } 9116} 9117CanvasSource.extension = ExtensionType.TextureSource; 9118 9119class ImageSource extends TextureSource { 9120 constructor(options) { 9121 super(options); 9122 this.uploadMethodId = "image"; 9123 this.autoGarbageCollect = true; 9124 } 9125 static test(resource) { 9126 return globalThis.HTMLImageElement && resource instanceof HTMLImageElement || typeof ImageBitmap !== "undefined" && resource instanceof ImageBitmap || globalThis.VideoFrame && resource instanceof VideoFrame; 9127 } 9128} 9129ImageSource.extension = ExtensionType.TextureSource; 9130 9131var UPDATE_PRIORITY = /* @__PURE__ */ ((UPDATE_PRIORITY2) => { 9132 UPDATE_PRIORITY2[UPDATE_PRIORITY2["INTERACTION"] = 50] = "INTERACTION"; 9133 UPDATE_PRIORITY2[UPDATE_PRIORITY2["HIGH"] = 25] = "HIGH"; 9134 UPDATE_PRIORITY2[UPDATE_PRIORITY2["NORMAL"] = 0] = "NORMAL"; 9135 UPDATE_PRIORITY2[UPDATE_PRIORITY2["LOW"] = -25] = "LOW"; 9136 UPDATE_PRIORITY2[UPDATE_PRIORITY2["UTILITY"] = -50] = "UTILITY"; 9137 return UPDATE_PRIORITY2; 9138})(UPDATE_PRIORITY || {}); 9139 9140class TickerListener { 9141 /** 9142 * Constructor 9143 * @private 9144 * @param fn - The listener function to be added for one update 9145 * @param context - The listener context 9146 * @param priority - The priority for emitting 9147 * @param once - If the handler should fire once 9148 */ 9149 constructor(fn, context = null, priority = 0, once = false) { 9150 /** The next item in chain. */ 9151 this.next = null; 9152 /** The previous item in chain. */ 9153 this.previous = null; 9154 /** `true` if this listener has been destroyed already. */ 9155 this._destroyed = false;
vendor: 2,140 bytes, lines 9156-9236
9156 this._fn = fn; 9157 this._context = context; 9158 this.priority = priority; 9159 this._once = once; 9160 } 9161 /** 9162 * Simple compare function to figure out if a function and context match. 9163 * @param fn - The listener function to be added for one update 9164 * @param context - The listener context 9165 * @returns `true` if the listener match the arguments 9166 */ 9167 match(fn, context = null) { 9168 return this._fn === fn && this._context === context; 9169 } 9170 /** 9171 * Emit by calling the current function. 9172 * @param ticker - The ticker emitting. 9173 * @returns Next ticker 9174 */ 9175 emit(ticker) { 9176 if (this._fn) { 9177 if (this._context) { 9178 this._fn.call(this._context, ticker); 9179 } else { 9180 this._fn(ticker); 9181 } 9182 } 9183 const redirect = this.next; 9184 if (this._once) { 9185 this.destroy(true); 9186 } 9187 if (this._destroyed) { 9188 this.next = null; 9189 } 9190 return redirect; 9191 } 9192 /** 9193 * Connect to the list. 9194 * @param previous - Input node, previous listener 9195 */ 9196 connect(previous) { 9197 this.previous = previous; 9198 if (previous.next) { 9199 previous.next.previous = this; 9200 } 9201 this.next = previous.next; 9202 previous.next = this; 9203 } 9204 /** 9205 * Destroy and don't use after this. 9206 * @param hard - `true` to remove the `next` reference, this 9207 * is considered a hard destroy. Soft destroy maintains the next reference. 9208 * @returns The listener to redirect while emitting or removing. 9209 */ 9210 destroy(hard = false) { 9211 this._destroyed = true; 9212 this._fn = null; 9213 this._context = null; 9214 if (this.previous) { 9215 this.previous.next = this.next; 9216 } 9217 if (this.next) { 9218 this.next.previous = this.previous; 9219 } 9220 const redirect = this.next; 9221 this.next = hard ? null : redirect; 9222 this.previous = null; 9223 return redirect; 9224 } 9225} 9226 9227const _Ticker = class _Ticker { 9228 constructor() { 9229 /** 9230 * Whether or not this ticker should invoke the method {@link Ticker#start|start} 9231 * automatically when a listener is added. 9232 * @example 9233 * ```ts 9234 * // Default behavior (manual start) 9235 * const ticker = new Ticker(); 9236 * ticker.autoStart = false;
9237 * ticker.add(() => { 9238 * // Won't run until ticker.start() is called 9239 * }); 9240 * 9241 * // Auto-start behavior 9242 * const autoTicker = new Ticker(); 9243 * autoTicker.autoStart = true; 9244 * autoTicker.add(() => { 9245 * // Runs immediately when added 9246 * }); 9247 * ``` 9248 * @default false 9249 * @see {@link Ticker#start} For manually starting the ticker 9250 * @see {@link Ticker#stop} For manually stopping the ticker 9251 */ 9252 this.autoStart = false; 9253 /** 9254 * Scalar representing the delta time factor. 9255 * This is a dimensionless value representing the fraction of a frame at the target framerate. 9256 * At 60 FPS, this value is typically around 1.0. 9257 * 9258 * This is NOT in milliseconds - it's a scalar multiplier for frame-independent animations. 9259 * For actual milliseconds, use {@link Ticker#deltaMS}. 9260 * @example 9261 * ```ts 9262 * // Frame-independent animation using deltaTime scalar 9263 * ticker.add((ticker) => { 9264 * // Rotate sprite by 0.1 radians per frame, scaled by deltaTime 9265 * sprite.rotation += 0.1 * ticker.deltaTime; 9266 * }); 9267 * ``` 9268 */ 9269 this.deltaTime = 1; 9270 /** 9271 * The last time update was invoked, in milliseconds since epoch. 9272 * Similar to performance.now() timestamp format. 9273 * 9274 * Used internally for calculating time deltas between frames. 9275 * @example 9276 * ```ts 9277 * ticker.add((ticker) => { 9278 * const currentTime = performance.now(); 9279 * const timeSinceLastFrame = currentTime - ticker.lastTime; 9280 * console.log(`Time since last frame: ${timeSinceLastFrame}ms`); 9281 * }); 9282 * ``` 9283 */ 9284 this.lastTime = -1; 9285 /** 9286 * Factor of current {@link Ticker#deltaTime|deltaTime}. 9287 * Used to scale time for slow motion or fast-forward effects. 9288 * @example 9289 * ```ts 9290 * // Basic speed adjustment 9291 * ticker.speed = 0.5; // Half speed (slow motion) 9292 * ticker.speed = 2.0; // Double speed (fast forward) 9293 * 9294 * // Temporary speed changes 9295 * function slowMotion() { 9296 * const normalSpeed = ticker.speed; 9297 * ticker.speed = 0.2; 9298 * setTimeout(() => { 9299 * ticker.speed = normalSpeed; 9300 * }, 1000); 9301 * } 9302 * ``` 9303 */ 9304 this.speed = 1; 9305 /** 9306 * Whether or not this ticker has been started. 9307 * 9308 * `true` if {@link Ticker#start|start} has been called. 9309 * `false` if {@link Ticker#stop|Stop} has been called. 9310 * 9311 * While `false`, this value may change to `true` in the 9312 * event of {@link Ticker#autoStart|autoStart} being `true` 9313 * and a listener is added. 9314 * @example 9315 * ```ts 9316 * // Check ticker state 9317 * const ticker = new Ticker(); 9318 * console.log(ticker.started); // false 9319 * 9320 * // Start and verify 9321 * ticker.start(); 9322 * console.log(ticker.started); // true 9323 * ``` 9324 */ 9325 this.started = false; 9326 /** Internal current frame request ID */ 9327 this._requestId = null; 9328 /** 9329 * Internal value managed by minFPS property setter and getter. 9330 * This is the maximum allowed milliseconds between updates. 9331 */ 9332 this._maxElapsedMS = 100; 9333 /** 9334 * Internal value managed by min
vendor: 11,311 bytes, lines 9334-9699
9334FPS property setter and getter. 9335 * This is the minimum allowed milliseconds between updates. 9336 */ 9337 this._minElapsedMS = 0; 9338 /** If enabled, deleting is disabled.*/ 9339 this._protected = false; 9340 /** The last time keyframe was executed. Maintains a relatively fixed interval with the previous value. */ 9341 this._lastFrame = -1; 9342 this._head = new TickerListener(null, null, Infinity); 9343 this.deltaMS = 1 / _Ticker.targetFPMS; 9344 this.elapsedMS = 1 / _Ticker.targetFPMS; 9345 this._tick = (time) => { 9346 this._requestId = null; 9347 if (this.started) { 9348 this.update(time); 9349 if (this.started && this._requestId === null && this._head.next) { 9350 this._requestId = requestAnimationFrame(this._tick); 9351 } 9352 } 9353 }; 9354 } 9355 /** 9356 * Conditionally requests a new animation frame. 9357 * If a frame has not already been requested, and if the internal 9358 * emitter has listeners, a new frame is requested. 9359 */ 9360 _requestIfNeeded() { 9361 if (this._requestId === null && this._head.next) { 9362 this.lastTime = performance.now(); 9363 this._lastFrame = this.lastTime; 9364 this._requestId = requestAnimationFrame(this._tick); 9365 } 9366 } 9367 /** Conditionally cancels a pending animation frame. */ 9368 _cancelIfNeeded() { 9369 if (this._requestId !== null) { 9370 cancelAnimationFrame(this._requestId); 9371 this._requestId = null; 9372 } 9373 } 9374 /** 9375 * Conditionally requests a new animation frame. 9376 * If the ticker has been started it checks if a frame has not already 9377 * been requested, and if the internal emitter has listeners. If these 9378 * conditions are met, a new frame is requested. If the ticker has not 9379 * been started, but autoStart is `true`, then the ticker starts now, 9380 * and continues with the previous conditions to request a new frame. 9381 */ 9382 _startIfPossible() { 9383 if (this.started) { 9384 this._requestIfNeeded(); 9385 } else if (this.autoStart) { 9386 this.start(); 9387 } 9388 } 9389 /** 9390 * Register a handler for tick events. 9391 * @param fn - The listener function to add. Receives the Ticker instance as parameter 9392 * @param context - The context for the listener 9393 * @param priority - The priority of the listener 9394 * @example 9395 * ```ts 9396 * // Access time properties through the ticker parameter 9397 * ticker.add((ticker) => { 9398 * // Use deltaTime (dimensionless scalar) for frame-independent animations 9399 * sprite.rotation += 0.1 * ticker.deltaTime; 9400 * 9401 * // Use deltaMS (milliseconds) for time-based calculations 9402 * const progress = ticker.deltaMS / animationDuration; 9403 * 9404 * // Use elapsedMS for raw timing measurements 9405 * console.log(`Raw frame time: ${ticker.elapsedMS}ms`); 9406 * }); 9407 * ``` 9408 */ 9409 add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { 9410 return this._addListener(new TickerListener(fn, context, priority)); 9411 } 9412 /** 9413 * Add a handler for the tick event which is only executed once on the next frame. 9414 * @example 9415 * ```ts 9416 * // Basic one-time update 9417 * ticker.addOnce(() => { 9418 * console.log('Runs next frame only'); 9419 * }); 9420 * 9421 * // With specific context 9422 * const game = { 9423 * init(ticker) { 9424 * this.loadResources(); 9425 * console.log('Game initialized'); 9426 * } 9427 * }; 9428 * ticker.addOnce(game.init, game); 9429 * 9430 * // With priority 9431 * ticker.addOnce( 9432 * () => { 9433 * // High priority one-time setup 9434 * physics.init(); 9435 * }, 9436 * undefined, 9437 * UPDATE_PRIORITY.HIGH 9438 * ); 9439 * ``` 9440 * @param fn - The listener function to be added for one update 9441 * @param context - The listener context 9442 * @param priority - The priority for emitting (default: UPDATE_PRIORITY.NORMAL) 9443 * @returns This instance of a ticker 9444 * @see {@link Ticker#add} For continuous updates 9445 * @see {@link Ticker#remove} For removing handlers 9446 */ 9447 addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { 9448 return this._addListener(new TickerListener(fn, context, priority, true)); 9449 } 9450 /** 9451 * Internally adds the event handler so that it can be sorted by priority. 9452 * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run 9453 * before the rendering. 9454 * @private 9455 * @param listener - Current listener being added. 9456 * @returns This instance of a ticker 9457 */ 9458 _addListener(listener) { 9459 let current = this._head.next; 9460 let previous = this._head; 9461 if (!current) { 9462 listener.connect(previous); 9463 } else { 9464 while (current) { 9465 if (listener.priority > current.priority) { 9466 listener.connect(previous); 9467 break; 9468 } 9469 previous = current; 9470 current = current.next; 9471 } 9472 if (!listener.previous) { 9473 listener.connect(previous); 9474 } 9475 } 9476 this._startIfPossible(); 9477 return this; 9478 } 9479 /** 9480 * Removes any handlers matching the function and context parameters. 9481 * If no handlers are left after removing, then it cancels the animation frame. 9482 * @example 9483 * ```ts 9484 * // Basic removal 9485 * const onTick = () => { 9486 * sprite.rotation += 0.1; 9487 * }; 9488 * ticker.add(onTick); 9489 * ticker.remove(onTick); 9490 * 9491 * // Remove with context 9492 * const game = { 9493 * update(ticker) { 9494 * this.physics.update(ticker.deltaTime); 9495 * } 9496 * }; 9497 * ticker.add(game.update, game); 9498 * ticker.remove(game.update, game); 9499 * 9500 * // Remove all matching handlers 9501 * // (if same function was added multiple times) 9502 * ticker.add(onTick); 9503 * ticker.add(onTick); 9504 * ticker.remove(onTick); // Removes all instances 9505 * ``` 9506 * @param fn - The listener function to be removed 9507 * @param context - The listener context to be removed 9508 * @returns This instance of a ticker 9509 * @see {@link Ticker#add} For adding handlers 9510 * @see {@link Ticker#addOnce} For one-time handlers 9511 */ 9512 remove(fn, context) { 9513 let listener = this._head.next; 9514 while (listener) { 9515 if (listener.match(fn, context)) { 9516 listener = listener.destroy(); 9517 } else { 9518 listener = listener.next; 9519 } 9520 } 9521 if (!this._head.next) { 9522 this._cancelIfNeeded(); 9523 } 9524 return this; 9525 } 9526 /** 9527 * The number of listeners on this ticker, calculated by walking through linked list. 9528 * @example 9529 * ```ts 9530 * // Check number of active listeners 9531 * const ticker = new Ticker(); 9532 * console.log(ticker.count); // 0 9533 * 9534 * // Add some listeners 9535 * ticker.add(() => {}); 9536 * ticker.add(() => {}); 9537 * console.log(ticker.count); // 2 9538 * 9539 * // Check after cleanup 9540 * ticker.destroy(); 9541 * console.log(ticker.count); // 0 9542 * ``` 9543 * @readonly 9544 * @see {@link Ticker#add} For adding listeners 9545 * @see {@link Ticker#remove} For removing listeners 9546 */ 9547 get count() { 9548 if (!this._head) { 9549 return 0; 9550 } 9551 let count = 0; 9552 let current = this._head; 9553 while (current = current.next) { 9554 count++; 9555 } 9556 return count; 9557 } 9558 /** 9559 * Starts the ticker. If the ticker has listeners a new animation frame is requested at this point. 9560 * @example 9561 * ```ts 9562 * // Basic manual start 9563 * const ticker = new Ticker(); 9564 * ticker.add(() => { 9565 * // Animation code here 9566 * }); 9567 * ticker.start(); 9568 * ``` 9569 * @see {@link Ticker#stop} For stopping the ticker 9570 * @see {@link Ticker#autoStart} For automatic starting 9571 * @see {@link Ticker#started} For checking ticker state 9572 */ 9573 start() { 9574 if (!this.started) { 9575 this.started = true; 9576 this._requestIfNeeded(); 9577 } 9578 } 9579 /** 9580 * Stops the ticker. If the ticker has requested an animation frame it is canceled at this point. 9581 * @example 9582 * ```ts 9583 * // Basic stop 9584 * const ticker = new Ticker(); 9585 * ticker.stop(); 9586 * ``` 9587 * @see {@link Ticker#start} For starting the ticker 9588 * @see {@link Ticker#started} For checking ticker state 9589 * @see {@link Ticker#destroy} For cleaning up the ticker 9590 */ 9591 stop() { 9592 if (this.started) { 9593 this.started = false; 9594 this._cancelIfNeeded(); 9595 } 9596 } 9597 /** 9598 * Destroy the ticker and don't use after this. Calling this method removes all references to internal events. 9599 * @example 9600 * ```ts 9601 * // Clean up with active listeners 9602 * const ticker = new Ticker(); 9603 * ticker.add(() => {}); 9604 * ticker.destroy(); // Removes all listeners 9605 * ``` 9606 * @see {@link Ticker#stop} For stopping without destroying 9607 * @see {@link Ticker#remove} For removing specific listeners 9608 */ 9609 destroy() { 9610 if (!this._protected) { 9611 this.stop(); 9612 let listener = this._head.next; 9613 while (listener) { 9614 listener = listener.destroy(true); 9615 } 9616 this._head.destroy(); 9617 this._head = null; 9618 } 9619 } 9620 /** 9621 * Triggers an update. 9622 * 9623 * An update entails setting the 9624 * current {@link Ticker#elapsedMS|elapsedMS}, 9625 * the current {@link Ticker#deltaTime|deltaTime}, 9626 * invoking all listeners with current deltaTime, 9627 * and then finally setting {@link Ticker#lastTime|lastTime} 9628 * with the value of currentTime that was provided. 9629 * 9630 * This method will be called automatically by animation 9631 * frame callbacks if the ticker instance has been started 9632 * and listeners are added. 9633 * @example 9634 * ```ts 9635 * // Basic manual update 9636 * const ticker = new Ticker(); 9637 * ticker.update(performance.now()); 9638 * ``` 9639 * @param currentTime - The current time of execution (defaults to performance.now()) 9640 * @see {@link Ticker#deltaTime} For frame delta value 9641 * @see {@link Ticker#elapsedMS} For raw elapsed time 9642 */ 9643 update(currentTime = performance.now()) { 9644 let elapsedMS; 9645 if (currentTime > this.lastTime) { 9646 elapsedMS = this.elapsedMS = currentTime - this.lastTime; 9647 if (elapsedMS > this._maxElapsedMS) { 9648 elapsedMS = this._maxElapsedMS; 9649 } 9650 elapsedMS *= this.speed; 9651 if (this._minElapsedMS) { 9652 const delta = currentTime - this._lastFrame | 0; 9653 if (delta < this._minElapsedMS) { 9654 return; 9655 } 9656 this._lastFrame = currentTime - delta % this._minElapsedMS; 9657 } 9658 this.deltaMS = elapsedMS; 9659 this.deltaTime = this.deltaMS * _Ticker.targetFPMS; 9660 const head = this._head; 9661 let listener = head.next; 9662 while (listener) { 9663 listener = listener.emit(this); 9664 } 9665 if (!head.next) { 9666 this._cancelIfNeeded(); 9667 } 9668 } else { 9669 this.deltaTime = this.deltaMS = this.elapsedMS = 0; 9670 } 9671 this.lastTime = currentTime; 9672 } 9673 /** 9674 * The frames per second at which this ticker is running. 9675 * The default is approximately 60 in most modern browsers. 9676 * > [!NOTE] This does not factor in the value of 9677 * > {@link Ticker#speed|speed}, which is specific 9678 * > to scaling {@link Ticker#deltaTime|deltaTime}. 9679 * @example 9680 * ```ts 9681 * // Basic FPS monitoring 9682 * ticker.add(() => { 9683 * console.log(`Current FPS: ${Math.round(ticker.FPS)}`); 9684 * }); 9685 * ``` 9686 * @readonly 9687 */ 9688 get FPS() { 9689 return 1e3 / this.elapsedMS; 9690 } 9691 /** 9692 * Manages the maximum amount of milliseconds allowed to 9693 * elapse between invoking {@link Ticker#update|update}. 9694 * 9695 * This value is used to cap {@link Ticker#deltaTime|deltaTime}, 9696 * but does not effect the measured value of {@link Ticker#FPS|FPS}. 9697 * 9698 * When setting this property it is clamped to a value between 9699 * `0` and `Ticker.targetFPMS * 1000`
9699. 9700 * @example 9701 * ```ts 9702 * // Set minimum acceptable frame rate 9703 * const ticker = new Ticker(); 9704 * ticker.minFPS = 30; // Never go below 30 FPS 9705 * 9706 * // Use with maxFPS for frame rate clamping 9707 * ticker.minFPS = 30; 9708 * ticker.maxFPS = 60; 9709 * 9710 * // Monitor delta capping 9711 * ticker.add(() => { 9712 * // Delta time will be capped based on minFPS 9713 * console.log(`Delta time: ${ticker.deltaTime}`); 9714 * }); 9715 * ``` 9716 * @default 10 9717 */ 9718 get minFPS() { 9719 return 1e3 / this._maxElapsedMS; 9720 } 9721 set minFPS(fps) { 9722 const minFPS = Math.min(this.maxFPS, fps); 9723 const minFPMS = Math.min(Math.max(0, minFPS) / 1e3, _Ticker.targetFPMS); 9724 this._maxElapsedMS = 1 / minFPMS; 9725 } 9726 /** 9727 * Manages the minimum amount of milliseconds required to 9728 * elapse between invoking {@link Ticker#update|update}. 9729 * 9730 * This will effect the measured value of {@link Ticker#FPS|FPS}. 9731 * 9732 * If it is set to `0`, then there is no limit; PixiJS will render as many frames as it can. 9733 * Otherwise it will be at least `minFPS` 9734 * @example 9735 * ```ts 9736 * // Set minimum acceptable frame rate 9737 * const ticker = new Ticker(); 9738 * ticker.maxFPS = 60; // Never go above 60 FPS 9739 * 9740 * // Use with maxFPS for frame rate clamping 9741 * ticker.minFPS = 30; 9742 * ticker.maxFPS = 60; 9743 * 9744 * // Monitor delta capping 9745 * ticker.add(() => { 9746 * // Delta time will be capped based on maxFPS 9747 * console.log(`Delta time: ${ticker.deltaTime}`); 9748 * }); 9749 * ``` 9750 * @default 0 9751 */ 9752 get maxFPS() { 9753 if (this._minElapsedMS) { 9754 return Math.round(1e3 / this._minElapsedMS); 9755 } 9756 return 0; 9757 } 9758 set maxFPS(fps) { 9759 if (fps === 0) { 9760 this._minElapsedMS = 0; 9761 } else { 9762 const maxFPS = Math.max(this.minFPS, fps); 9763 this._minElapsedMS = 1 / (maxFPS
vendor: 3,151 bytes, lines 9763-9860
9763 / 1e3); 9764 } 9765 } 9766 /** 9767 * The shared ticker instance used by {@link AnimatedSprite} and by 9768 * {@link VideoSource} to update animation frames / video textures. 9769 * 9770 * It may also be used by {@link Application} if created with the `sharedTicker` option property set to true. 9771 * 9772 * The property {@link Ticker#autoStart|autoStart} is set to `true` for this instance. 9773 * Please follow the examples for usage, including how to opt-out of auto-starting the shared ticker. 9774 * @example 9775 * import { Ticker } from 'pixi.js'; 9776 * 9777 * const ticker = Ticker.shared; 9778 * // Set this to prevent starting this ticker when listeners are added. 9779 * // By default this is true only for the Ticker.shared instance. 9780 * ticker.autoStart = false; 9781 * 9782 * // FYI, call this to ensure the ticker is stopped. It should be stopped 9783 * // if you have not attempted to render anything yet. 9784 * ticker.stop(); 9785 * 9786 * // Call this when you are ready for a running shared ticker. 9787 * ticker.start(); 9788 * @example 9789 * import { autoDetectRenderer, Container } from 'pixi.js'; 9790 * 9791 * // You may use the shared ticker to render... 9792 * const renderer = autoDetectRenderer(); 9793 * const stage = new Container(); 9794 * document.body.appendChild(renderer.view); 9795 * ticker.add((time) => renderer.render(stage)); 9796 * 9797 * // Or you can just update it manually. 9798 * ticker.autoStart = false; 9799 * ticker.stop(); 9800 * const animate = (time) => { 9801 * ticker.update(time); 9802 * renderer.render(stage); 9803 * requestAnimationFrame(animate); 9804 * }; 9805 * animate(performance.now()); 9806 * @type {Ticker} 9807 * @readonly 9808 */ 9809 static get shared() { 9810 if (!_Ticker._shared) { 9811 const shared = _Ticker._shared = new _Ticker(); 9812 shared.autoStart = true; 9813 shared._protected = true; 9814 } 9815 return _Ticker._shared; 9816 } 9817 /** 9818 * The system ticker instance used by {@link PrepareBase} for core timing 9819 * functionality that shouldn't usually need to be paused, unlike the `shared` 9820 * ticker which drives visual animations and rendering which may want to be paused. 9821 * 9822 * The property {@link Ticker#autoStart|autoStart} is set to `true` for this instance. 9823 * @type {Ticker} 9824 * @readonly 9825 * @advanced 9826 */ 9827 static get system() { 9828 if (!_Ticker._system) { 9829 const system = _Ticker._system = new _Ticker(); 9830 system.autoStart = true; 9831 system._protected = true; 9832 } 9833 return _Ticker._system; 9834 } 9835}; 9836/** 9837 * Target frame rate in frames per millisecond. 9838 * Used for converting deltaTime to a scalar time delta. 9839 * @example 9840 * ```ts 9841 * // Default is 0.06 (60 FPS) 9842 * console.log(Ticker.targetFPMS); // 0.06 9843 * 9844 * // Calculate target frame duration 9845 * const frameDuration = 1 / Ticker.targetFPMS; // ≈ 16.67ms 9846 * 9847 * // Use in custom timing calculations 9848 * const deltaTime = elapsedMS * Ticker.targetFPMS; 9849 * ``` 9850 * @remarks 9851 * - Default is 0.06 (equivalent to 60 FPS) 9852 * - Used in deltaTime calculations 9853 * - Affects all ticker instances 9854 * @default 0.06 9855 * @see {@link Ticker#deltaTime} For time scaling 9856 * @see {@link Ticker#FPS} For actual frame rate 9857 */ 9858_Ticker.targetFPMS = 0.06; 9859let Ticker = _Ticker; 9860
9861let promise; 9862async function detectVideoAlphaMode() { 9863 promise ?? (promise = (async () => { 9864 const canvas = DOMAdapter.get().createCanvas(1, 1); 9865 const gl = canvas.getContext("webgl"); 9866 if (!gl) { 9867 return "premultiply-alpha-on-upload"; 9868 } 9869 const video = await new Promise((resolve) => { 9870 const video2 = document.createElement("video"); 9871 video2.onloadeddata = () => resolve(video2); 9872 video2.onerror = () => resolve(null); 9873 video2.autoplay = false; 9874 video2.crossOrigin = "anonymous"; 9875 video2.preload = "auto"; 9876 video2.src = "data:video/webm;base64,GkXfo59ChoEBQveBAULygQRC84EIQoKEd2VibUKHgQJChYECGFOAZwEAAAAAAAHTEU2bdLpNu4tTq4QVSalmU6yBoU27i1OrhBZUrmtTrIHGTbuMU6uEElTDZ1OsggEXTbuMU6uEHFO7a1OsggG97AEAAAAAAABZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmoCrXsYMPQkBNgIRMYXZmV0GETGF2ZkSJiEBEAAAAAAAAFlSua8yuAQAAAAAAAEPXgQFzxYgAAAAAAAAAAZyBACK1nIN1bmSIgQCGhVZfVlA5g4EBI+ODhAJiWgDglLCBArqBApqBAlPAgQFVsIRVuYEBElTDZ9Vzc9JjwItjxYgAAAAAAAAAAWfInEWjh0VOQ09ERVJEh49MYXZjIGxpYnZweC12cDlnyKJFo4hEVVJBVElPTkSHlDAwOjAwOjAwLjA0MDAwMDAwMAAAH0O2dcfngQCgwqGggQAAAIJJg0IAABAAFgA4JBwYSgAAICAAEb///4r+AAB1oZ2mm+6BAaWWgkmDQgAAEAAWADgkHBhKAAAgIABIQBxTu2uRu4+zgQC3iveBAfGCAXHwgQM="; 9877 video2.load(); 9878 }); 9879 if (!video) { 9880 return "premultiply-alpha-on-upload"; 9881 } 9882 const texture = gl.createTexture(); 9883 gl.bindTexture(gl.TEXTURE_2D, texture); 9884 const framebuffer = gl.createFramebuffer(); 9885 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 9886 gl.framebufferTexture2D( 9887 gl.FRAMEBUFFER, 9888 gl.COLOR_ATTACHMENT0, 9889 gl.TEXTURE_2D, 9890 texture, 9891 0 9892 ); 9893 gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); 9894 gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); 9895 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video); 9896 const pixel = new Uint8Array(4); 9897 gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); 9898 gl.deleteFramebuffer(framebuffer); 9899 gl.deleteTexture(texture); 9900 gl.getExtension("WEBGL_lose_context")?.loseContext(); 9901 return pixel[0] <= pixel[3] ? "premultiplied-alpha" : "premultiply-alpha-on-upload"; 9902 })()); 9903 return promise; 9904} 9905 9906const _VideoSource = class _VideoSource extends TextureSource { 9907 constructor(options) { 9908 super(options); 9909 // Public 9910 /** Whether or not the video is ready to play. */ 9911 this.isReady = false; 9912 /** The upload method for this texture. */ 9913 this.uploadMethodId = "video"; 9914 options = { 9915 ..._VideoSource.defaultOptions, 9916 ...options 9917 }; 9918 this._autoUpdate = true; 9919 this._isConnectedToTicker = false; 9920 this._updateFPS = options.updateFPS || 0; 9921 this._msToNextUpdate = 0; 9922 this.autoPlay = options.autoPlay !== false; 9923 this.alphaMode = options.alphaMode ?? "premultiply-alpha-on-upload"; 9924 this._videoFrameRequestCallback = this._videoFrameRequestCallback.bind(this); 9925 this._videoFrameRequestCallbackHandle = null; 9926 this._load = null; 9927 this._resolve = null; 9928 this._reject = null; 9929 this._onCanPlay = this._onCanPlay.bind(this); 9930 this._onCanPlayThrough = this._onCanPlayThrough.bind(this); 9931 this._onError = this._onError.bind(this); 9932 this._onPlayStart = this._onPlayStart.bind(this); 9933 this._onPlayStop = this._onPlayStop.bind(this); 9934 this._onSeeked = this._onSeeked.bind(this); 9935 if (options.autoLoad !== false) { 9936 void this.load(); 9937 } 9938 } 9939 /** Update the video frame if the source is not destroyed and meets certain conditions. */ 9940 updateFrame() { 9941 if (this.destroyed) { 9942 return; 9943 } 9944 if (this._updateFPS) { 9945 const elapsedMS = Ticker.shared.elapsedMS * this.resource.playbackRate; 9946 this._msToNextUpdate = Math.floor(this._msToNextUpdate - elapsedMS); 9947 } 9948 if (!this._updateFPS || this._msToNextUpdate <= 0) { 9949 this._msToNextUpdate = this._updateFPS ? Math.floor(1e3 / this._updateFPS) : 0; 9950 } 9951 if (this.isValid) { 9952 this.update(); 9953 } 9954 } 9955 /** Callback to update the video frame and potentially request the next frame update. */ 9956 _videoFrameRequestCallback() { 9957 this.updateFrame(); 9958 if (this.destroyed) { 9959 this._videoFrameRequestCallbackHandle = null; 9960 } else { 9961 this._videoFrameRequestCallbackHandle = this.resource.requestVideoFrameCallback( 9962 this._videoFrameRequestCallback 9963 ); 9964 } 9965 } 9966 /** 9967 * Checks if the resource has valid dimensions. 9968 * @returns {boolean} True if width and height are set, otherwise false. 9969 */ 9970 get isValid() { 9971 return !!this.resource.videoWidth && !!this.resource.videoHeight; 9972 } 9973 /** 9974 * Start preloading the video resource. 9975 * @returns {Promise<this>} Handle the validate event 9976 */ 9977 async load() { 9978 if (this._load) { 9979 return this._load; 9980 } 9981 const source = this.resource; 9982 const options = this.options; 9983 if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) && source.width && source.height) { 9984 source.complete = true; 9985 } 9986 source.addEventListener("play", this._onPlayStart); 9987 source.addEventListener("pause", this._onPlayStop); 9988 source.addEventListener("seeked", this._onSeeked); 9989 if (!this._isSourceReady()) { 9990 if (!options.preload) { 9991 source.addEventListener("canplay", this._onCanPlay); 9992 } 9993 source.addEventListener("canplaythrough", this._onCanPlayThrough); 9994 source.addEventListener("error", this._onError, true); 9995 } else { 9996 this._mediaReady(); 9997 } 9998 this.alphaMode = await detectVideoAlphaMode(); 9999 this._load = new Promise((resolve, reject) => { 10000 if (this.isValid) { 10001 resolve(this); 10002 } else { 10003 this._resolve = resolve; 10004 this._reject = reject; 10005 if (options.preloadTimeoutMs !== void 0) { 10006 this._preloadTimeout = setTimeout(() => { 10007 this._onError(new ErrorEvent(`Preload exceeded timeout of ${options.preloadTimeoutMs}ms`)); 10008 }); 10009 } 10010 source.load(); 10011 } 10012 }); 10013 return this._load; 10014 } 10015 /** 10016 * Handle video error events. 10017 * @param event - The error event 10018 */ 10019 _onError(event) { 10020 this.resource.removeEventListener("error", this._onError, true);
vendor: 22,330 bytes, lines 10021-10757
10021 this.emit("error", event); 10022 if (this._reject) { 10023 this._reject(event); 10024 this._reject = null; 10025 this._resolve = null; 10026 } 10027 } 10028 /** 10029 * Checks if the underlying source is playing. 10030 * @returns True if playing. 10031 */ 10032 _isSourcePlaying() { 10033 const source = this.resource; 10034 return !source.paused && !source.ended; 10035 } 10036 /** 10037 * Checks if the underlying source is ready for playing. 10038 * @returns True if ready. 10039 */ 10040 _isSourceReady() { 10041 const source = this.resource; 10042 return source.readyState > 2; 10043 } 10044 /** Runs the update loop when the video is ready to play. */ 10045 _onPlayStart() { 10046 if (!this.isValid) { 10047 this._mediaReady(); 10048 } 10049 this._configureAutoUpdate(); 10050 } 10051 /** Stops the update loop when a pause event is triggered. */ 10052 _onPlayStop() { 10053 this._configureAutoUpdate(); 10054 } 10055 /** Handles behavior when the video completes seeking to the current playback position. */ 10056 _onSeeked() { 10057 if (this._autoUpdate && !this._isSourcePlaying()) { 10058 this._msToNextUpdate = 0; 10059 this.updateFrame(); 10060 this._msToNextUpdate = 0; 10061 } 10062 } 10063 _onCanPlay() { 10064 const source = this.resource; 10065 source.removeEventListener("canplay", this._onCanPlay); 10066 this._mediaReady(); 10067 } 10068 _onCanPlayThrough() { 10069 const source = this.resource; 10070 source.removeEventListener("canplaythrough", this._onCanPlay); 10071 if (this._preloadTimeout) { 10072 clearTimeout(this._preloadTimeout); 10073 this._preloadTimeout = void 0; 10074 } 10075 this._mediaReady(); 10076 } 10077 /** Fired when the video is loaded and ready to play. */ 10078 _mediaReady() { 10079 const source = this.resource; 10080 if (this.isValid) { 10081 this.isReady = true; 10082 this.resize(source.videoWidth, source.videoHeight); 10083 } 10084 this._msToNextUpdate = 0; 10085 this.updateFrame(); 10086 this._msToNextUpdate = 0; 10087 if (this._resolve) { 10088 this._resolve(this); 10089 this._resolve = null; 10090 this._reject = null; 10091 } 10092 if (this._isSourcePlaying()) { 10093 this._onPlayStart(); 10094 } else if (this.autoPlay) { 10095 void this.resource.play(); 10096 } 10097 } 10098 /** Cleans up resources and event listeners associated with this texture. */ 10099 destroy() { 10100 this._configureAutoUpdate(); 10101 const source = this.resource; 10102 if (source) { 10103 source.removeEventListener("play", this._onPlayStart); 10104 source.removeEventListener("pause", this._onPlayStop); 10105 source.removeEventListener("seeked", this._onSeeked); 10106 source.removeEventListener("canplay", this._onCanPlay); 10107 source.removeEventListener("canplaythrough", this._onCanPlayThrough); 10108 source.removeEventListener("error", this._onError, true); 10109 source.pause(); 10110 source.src = ""; 10111 source.load(); 10112 } 10113 super.destroy(); 10114 } 10115 /** Should the base texture automatically update itself, set to true by default. */ 10116 get autoUpdate() { 10117 return this._autoUpdate; 10118 } 10119 set autoUpdate(value) { 10120 if (value !== this._autoUpdate) { 10121 this._autoUpdate = value; 10122 this._configureAutoUpdate(); 10123 } 10124 } 10125 /** 10126 * How many times a second to update the texture from the video. 10127 * Leave at 0 to update at every render. 10128 * A lower fps can help performance, as updating the texture at 60fps on a 30ps video may not be efficient. 10129 */ 10130 get updateFPS() { 10131 return this._updateFPS; 10132 } 10133 set updateFPS(value) { 10134 if (value !== this._updateFPS) { 10135 this._updateFPS = value; 10136 this._configureAutoUpdate(); 10137 } 10138 } 10139 /** 10140 * Configures the updating mechanism based on the current state and settings. 10141 * 10142 * This method decides between using the browser's native video frame callback or a custom ticker 10143 * for updating the video frame. It ensures optimal performance and responsiveness 10144 * based on the video's state, playback status, and the desired frames-per-second setting. 10145 * 10146 * - If `_autoUpdate` is enabled and the video source is playing: 10147 * - It will prefer the native video frame callback if available and no specific FPS is set. 10148 * - Otherwise, it will use a custom ticker for manual updates. 10149 * - If `_autoUpdate` is disabled or the video isn't playing, any active update mechanisms are halted. 10150 */ 10151 _configureAutoUpdate() { 10152 if (this._autoUpdate && this._isSourcePlaying()) { 10153 if (!this._updateFPS && this.resource.requestVideoFrameCallback) { 10154 if (this._isConnectedToTicker) { 10155 Ticker.shared.remove(this.updateFrame, this); 10156 this._isConnectedToTicker = false; 10157 this._msToNextUpdate = 0; 10158 } 10159 if (this._videoFrameRequestCallbackHandle === null) { 10160 this._videoFrameRequestCallbackHandle = this.resource.requestVideoFrameCallback( 10161 this._videoFrameRequestCallback 10162 ); 10163 } 10164 } else { 10165 if (this._videoFrameRequestCallbackHandle !== null) { 10166 this.resource.cancelVideoFrameCallback(this._videoFrameRequestCallbackHandle); 10167 this._videoFrameRequestCallbackHandle = null; 10168 } 10169 if (!this._isConnectedToTicker) { 10170 Ticker.shared.add(this.updateFrame, this); 10171 this._isConnectedToTicker = true; 10172 this._msToNextUpdate = 0; 10173 } 10174 } 10175 } else { 10176 if (this._videoFrameRequestCallbackHandle !== null) { 10177 this.resource.cancelVideoFrameCallback(this._videoFrameRequestCallbackHandle); 10178 this._videoFrameRequestCallbackHandle = null; 10179 } 10180 if (this._isConnectedToTicker) { 10181 Ticker.shared.remove(this.updateFrame, this); 10182 this._isConnectedToTicker = false; 10183 this._msToNextUpdate = 0; 10184 } 10185 } 10186 } 10187 static test(resource) { 10188 return globalThis.HTMLVideoElement && resource instanceof HTMLVideoElement; 10189 } 10190}; 10191_VideoSource.extension = ExtensionType.TextureSource; 10192/** The default options for video sources. */ 10193_VideoSource.defaultOptions = { 10194 ...TextureSource.defaultOptions, 10195 /** If true, the video will start loading immediately. */ 10196 autoLoad: true, 10197 /** If true, the video will start playing as soon as it is loaded. */ 10198 autoPlay: true, 10199 /** The number of times a second to update the texture from the video. Leave at 0 to update at every render. */ 10200 updateFPS: 0, 10201 /** If true, the video will be loaded with the `crossorigin` attribute. */ 10202 crossorigin: true, 10203 /** If true, the video will loop when it ends. */ 10204 loop: false, 10205 /** If true, the video will be muted. */ 10206 muted: true, 10207 /** If true, the video will play inline. */ 10208 playsinline: true, 10209 /** If true, the video will be preloaded. */ 10210 preload: false 10211}; 10212/** 10213 * Map of video MIME types that can't be directly derived from file extensions. 10214 * @readonly 10215 */ 10216_VideoSource.MIME_TYPES = { 10217 ogv: "video/ogg", 10218 mov: "video/quicktime", 10219 m4v: "video/mp4" 10220}; 10221let VideoSource = _VideoSource; 10222 10223const convertToList = (input, transform, forceTransform = false) => { 10224 if (!Array.isArray(input)) { 10225 input = [input]; 10226 } 10227 if (!transform) { 10228 return input; 10229 } 10230 return input.map((item) => { 10231 if (typeof item === "string" || forceTransform) { 10232 return transform(item); 10233 } 10234 return item; 10235 }); 10236}; 10237 10238class CacheClass { 10239 constructor() { 10240 this._parsers = []; 10241 this._cache = /* @__PURE__ */ new Map(); 10242 this._cacheMap = /* @__PURE__ */ new Map(); 10243 } 10244 /** Clear all entries. */ 10245 reset() { 10246 this._cacheMap.clear(); 10247 this._cache.clear(); 10248 } 10249 /** 10250 * Check if the key exists 10251 * @param key - The key to check 10252 */ 10253 has(key) { 10254 return this._cache.has(key); 10255 } 10256 /** 10257 * Fetch entry by key 10258 * @param key - The key of the entry to get 10259 */ 10260 get(key) { 10261 const result = this._cache.get(key); 10262 if (!result) { 10263 warn(`[Assets] Asset id ${key} was not found in the Cache`); 10264 } 10265 return result; 10266 } 10267 /** 10268 * Set a value by key or keys name 10269 * @param key - The key or keys to set 10270 * @param value - The value to store in the cache or from which cacheable assets will be derived. 10271 */ 10272 set(key, value) { 10273 const keys = convertToList(key); 10274 let cacheableAssets; 10275 for (let i = 0; i < this.parsers.length; i++) { 10276 const parser = this.parsers[i]; 10277 if (parser.test(value)) { 10278 cacheableAssets = parser.getCacheableAssets(keys, value); 10279 break; 10280 } 10281 } 10282 const cacheableMap = new Map(Object.entries(cacheableAssets || {})); 10283 if (!cacheableAssets) { 10284 keys.forEach((key2) => { 10285 cacheableMap.set(key2, value); 10286 }); 10287 } 10288 const cacheKeys = [...cacheableMap.keys()]; 10289 const cachedAssets = { 10290 cacheKeys, 10291 keys 10292 }; 10293 keys.forEach((key2) => { 10294 this._cacheMap.set(key2, cachedAssets); 10295 }); 10296 cacheKeys.forEach((key2) => { 10297 const val = cacheableAssets ? cacheableAssets[key2] : value; 10298 if (this._cache.has(key2) && this._cache.get(key2) !== val) { 10299 warn("[Cache] already has key:", key2); 10300 } 10301 this._cache.set(key2, cacheableMap.get(key2)); 10302 }); 10303 } 10304 /** 10305 * Remove entry by key 10306 * 10307 * This function will also remove any associated alias from the cache also. 10308 * @param key - The key of the entry to remove 10309 */ 10310 remove(key) { 10311 if (!this._cacheMap.has(key)) { 10312 warn(`[Assets] Asset id ${key} was not found in the Cache`); 10313 return; 10314 } 10315 const cacheMap = this._cacheMap.get(key); 10316 const cacheKeys = cacheMap.cacheKeys; 10317 cacheKeys.forEach((key2) => { 10318 this._cache.delete(key2); 10319 }); 10320 cacheMap.keys.forEach((key2) => { 10321 this._cacheMap.delete(key2); 10322 }); 10323 } 10324 /** 10325 * All loader parsers registered 10326 * @advanced 10327 */ 10328 get parsers() { 10329 return this._parsers; 10330 } 10331} 10332const Cache = new CacheClass(); 10333 10334const sources = []; 10335extensions.handleByList(ExtensionType.TextureSource, sources); 10336function textureSourceFrom(options = {}) { 10337 const hasResource = options && options.resource; 10338 const res = hasResource ? options.resource : options; 10339 const opts = hasResource ? options : { resource: options }; 10340 for (let i = 0; i < sources.length; i++) { 10341 const Source = sources[i]; 10342 if (Source.test(res)) { 10343 return new Source(opts); 10344 } 10345 } 10346 throw new Error(`Could not find a source type for resource: ${opts.resource}`); 10347} 10348function resourceToTexture(options = {}, skipCache = false) { 10349 const hasResource = options && options.resource; 10350 const resource = hasResource ? options.resource : options; 10351 const opts = hasResource ? options : { resource: options }; 10352 if (!skipCache && Cache.has(resource)) { 10353 return Cache.get(resource); 10354 } 10355 const texture = new Texture({ source: textureSourceFrom(opts) }); 10356 texture.on("destroy", () => { 10357 if (Cache.has(resource)) { 10358 Cache.remove(resource); 10359 } 10360 }); 10361 if (!skipCache) { 10362 Cache.set(resource, texture); 10363 } 10364 return texture; 10365} 10366function textureFrom(id, skipCache = false) { 10367 if (typeof id === "string") { 10368 return Cache.get(id); 10369 } else if (id instanceof TextureSource) { 10370 return new Texture({ source: id }); 10371 } 10372 return resourceToTexture(id, skipCache); 10373} 10374Texture.from = textureFrom; 10375TextureSource.from = textureSourceFrom; 10376 10377extensions.add(AlphaMask, ColorMask, StencilMask, VideoSource, ImageSource, CanvasSource, BufferImageSource); 10378 10379var LoaderParserPriority = /* @__PURE__ */ ((LoaderParserPriority2) => { 10380 LoaderParserPriority2[LoaderParserPriority2["Low"] = 0] = "Low"; 10381 LoaderParserPriority2[LoaderParserPriority2["Normal"] = 1] = "Normal"; 10382 LoaderParserPriority2[LoaderParserPriority2["High"] = 2] = "High"; 10383 return LoaderParserPriority2; 10384})(LoaderParserPriority || {}); 10385 10386function assertPath(path2) { 10387 if (typeof path2 !== "string") { 10388 throw new TypeError(`Path must be a string. Received ${JSON.stringify(path2)}`); 10389 } 10390} 10391function removeUrlParams(url) { 10392 const re = url.split("?")[0]; 10393 return re.split("#")[0]; 10394} 10395function escapeRegExp(string) { 10396 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 10397} 10398function replaceAll(str, find, replace) { 10399 return str.replace(new RegExp(escapeRegExp(find), "g"), replace); 10400} 10401function normalizeStringPosix(path2, allowAboveRoot) { 10402 let res = ""; 10403 let lastSegmentLength = 0; 10404 let lastSlash = -1; 10405 let dots = 0; 10406 let code = -1; 10407 for (let i = 0; i <= path2.length; ++i) { 10408 if (i < path2.length) { 10409 code = path2.charCodeAt(i); 10410 } else if (code === 47) { 10411 break; 10412 } else { 10413 code = 47; 10414 } 10415 if (code === 47) { 10416 if (lastSlash === i - 1 || dots === 1) ; else if (lastSlash !== i - 1 && dots === 2) { 10417 if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) { 10418 if (res.length > 2) { 10419 const lastSlashIndex = res.lastIndexOf("/"); 10420 if (lastSlashIndex !== res.length - 1) { 10421 if (lastSlashIndex === -1) { 10422 res = ""; 10423 lastSegmentLength = 0; 10424 } else { 10425 res = res.slice(0, lastSlashIndex); 10426 lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); 10427 } 10428 lastSlash = i; 10429 dots = 0; 10430 continue; 10431 } 10432 } else if (res.length === 2 || res.length === 1) { 10433 res = ""; 10434 lastSegmentLength = 0; 10435 lastSlash = i; 10436 dots = 0; 10437 continue; 10438 } 10439 } 10440 } else { 10441 if (res.length > 0) { 10442 res += `/${path2.slice(lastSlash + 1, i)}`; 10443 } else { 10444 res = path2.slice(lastSlash + 1, i); 10445 } 10446 lastSegmentLength = i - lastSlash - 1; 10447 } 10448 lastSlash = i; 10449 dots = 0; 10450 } else if (code === 46 && dots !== -1) { 10451 ++dots; 10452 } else { 10453 dots = -1; 10454 } 10455 } 10456 return res; 10457} 10458const path = { 10459 /** 10460 * Converts a path to posix format. 10461 * @param path - The path to convert to posix 10462 * @example 10463 * ```ts 10464 * // Convert a Windows path to POSIX format 10465 * path.toPosix('C:\\Users\\User\\Documents\\file.txt'); 10466 * // -> 'C:/Users/User/Documents/file.txt' 10467 * ``` 10468 */ 10469 toPosix(path2) { 10470 return replaceAll(path2, "\\", "/"); 10471 }, 10472 /** 10473 * Checks if the path is a URL e.g. http://, https:// 10474 * @param path - The path to check 10475 * @example 10476 * ```ts 10477 * // Check if a path is a URL 10478 * path.isUrl('http://www.example.com'); 10479 * // -> true 10480 * path.isUrl('C:/Users/User/Documents/file.txt'); 10481 * // -> false 10482 * ``` 10483 */ 10484 isUrl(path2) { 10485 return /^https?:/.test(this.toPosix(path2)); 10486 }, 10487 /** 10488 * Checks if the path is a data URL 10489 * @param path - The path to check 10490 * @example 10491 * ```ts 10492 * // Check if a path is a data URL 10493 * path.isDataUrl('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...'); 10494 * // -> true 10495 * ``` 10496 */ 10497 isDataUrl(path2) { 10498 return /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}()_|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s<>]*?)$/i.test(path2); 10499 }, 10500 /** 10501 * Checks if the path is a blob URL 10502 * @param path - The path to check 10503 * @example 10504 * ```ts 10505 * // Check if a path is a blob URL 10506 * path.isBlobUrl('blob:http://www.example.com/12345678-1234-1234-1234-123456789012'); 10507 * // -> true 10508 * ``` 10509 */ 10510 isBlobUrl(path2) { 10511 return path2.startsWith("blob:"); 10512 }, 10513 /** 10514 * Checks if the path has a protocol e.g. http://, https://, file:///, data:, blob:, C:/ 10515 * This will return true for windows file paths 10516 * @param path - The path to check 10517 * @example 10518 * ```ts 10519 * // Check if a path has a protocol 10520 * path.hasProtocol('http://www.example.com'); 10521 * // -> true 10522 * path.hasProtocol('C:/Users/User/Documents/file.txt'); 10523 * // -> true 10524 * ``` 10525 */ 10526 hasProtocol(path2) { 10527 return /^[^/:]+:/.test(this.toPosix(path2)); 10528 }, 10529 /** 10530 * Returns the protocol of the path e.g. http://, https://, file:///, data:, blob:, C:/ 10531 * @param path - The path to get the protocol from 10532 * @example 10533 * ```ts 10534 * // Get the protocol from a URL 10535 * path.getProtocol('http://www.example.com/path/to/resource'); 10536 * // -> 'http://' 10537 * // Get the protocol from a file path 10538 * path.getProtocol('C:/Users/User/Documents/file.txt'); 10539 * // -> 'C:/' 10540 * ``` 10541 */ 10542 getProtocol(path2) { 10543 assertPath(path2); 10544 path2 = this.toPosix(path2); 10545 const matchFile = /^file:\/\/\//.exec(path2); 10546 if (matchFile) { 10547 return matchFile[0]; 10548 } 10549 const matchProtocol = /^[^/:]+:\/{0,2}/.exec(path2); 10550 if (matchProtocol) { 10551 return matchProtocol[0]; 10552 } 10553 return ""; 10554 }, 10555 /** 10556 * Converts URL to an absolute path. 10557 * When loading from a Web Worker, we must use absolute paths. 10558 * If the URL is already absolute we return it as is 10559 * If it's not, we convert it 10560 * @param url - The URL to test 10561 * @param customBaseUrl - The base URL to use 10562 * @param customRootUrl - The root URL to use 10563 * @example 10564 * ```ts 10565 * // Convert a relative URL to an absolute path 10566 * path.toAbsolute('images/texture.png', 'http://example.com/assets/'); 10567 * // -> 'http://example.com/assets/images/texture.png' 10568 * ``` 10569 */ 10570 toAbsolute(url, customBaseUrl, customRootUrl) { 10571 assertPath(url); 10572 if (this.isDataUrl(url) || this.isBlobUrl(url)) 10573 return url; 10574 const baseUrl = removeUrlParams(this.toPosix(customBaseUrl ?? DOMAdapter.get().getBaseUrl())); 10575 const rootUrl = removeUrlParams(this.toPosix(customRootUrl ?? this.rootname(baseUrl))); 10576 url = this.toPosix(url); 10577 if (url.startsWith("/")) { 10578 return path.join(rootUrl, url.slice(1)); 10579 } 10580 const absolutePath = this.isAbsolute(url) ? url : this.join(baseUrl, url); 10581 return absolutePath; 10582 }, 10583 /** 10584 * Normalizes the given path, resolving '..' and '.' segments 10585 * @param path - The path to normalize 10586 * @example 10587 * ```ts 10588 * // Normalize a path with relative segments 10589 * path.normalize('http://www.example.com/foo/bar/../baz'); 10590 * // -> 'http://www.example.com/foo/baz' 10591 * // Normalize a file path with relative segments 10592 * path.normalize('C:\\Users\\User\\Documents\\..\\file.txt'); 10593 * // -> 'C:/Users/User/file.txt' 10594 * ``` 10595 */ 10596 normalize(path2) { 10597 assertPath(path2); 10598 if (path2.length === 0) 10599 return "."; 10600 if (this.isDataUrl(path2) || this.isBlobUrl(path2)) 10601 return path2; 10602 path2 = this.toPosix(path2); 10603 let protocol = ""; 10604 const isAbsolute = path2.startsWith("/"); 10605 if (this.hasProtocol(path2)) { 10606 protocol = this.rootname(path2); 10607 path2 = path2.slice(protocol.length); 10608 } 10609 const trailingSeparator = path2.endsWith("/"); 10610 path2 = normalizeStringPosix(path2); 10611 if (path2.length > 0 && trailingSeparator) 10612 path2 += "/"; 10613 if (isAbsolute) 10614 return `/${path2}`; 10615 return protocol + path2; 10616 }, 10617 /** 10618 * Determines if path is an absolute path. 10619 * Absolute paths can be urls, data urls, or paths on disk 10620 * @param path - The path to test 10621 * @example 10622 * ```ts 10623 * // Check if a path is absolute 10624 * path.isAbsolute('http://www.example.com/foo/bar'); 10625 * // -> true 10626 * path.isAbsolute('C:/Users/User/Documents/file.txt'); 10627 * // -> true 10628 * ``` 10629 */ 10630 isAbsolute(path2) { 10631 assertPath(path2); 10632 path2 = this.toPosix(path2); 10633 if (this.hasProtocol(path2)) 10634 return true; 10635 return path2.startsWith("/"); 10636 }, 10637 /** 10638 * Joins all given path segments together using the platform-specific separator as a delimiter, 10639 * then normalizes the resulting path 10640 * @param segments - The segments of the path to join 10641 * @example 10642 * ```ts 10643 * // Join multiple path segments 10644 * path.join('assets', 'images', 'sprite.png'); 10645 * // -> 'assets/images/sprite.png' 10646 * // Join with relative segments 10647 * path.join('assets', 'images', '../textures', 'sprite.png'); 10648 * // -> 'assets/textures/sprite.png' 10649 * ``` 10650 */ 10651 join(...segments) { 10652 if (segments.length === 0) { 10653 return "."; 10654 } 10655 let joined; 10656 for (let i = 0; i < segments.length; ++i) { 10657 const arg = segments[i]; 10658 assertPath(arg); 10659 if (arg.length > 0) { 10660 if (joined === void 0) 10661 joined = arg; 10662 else { 10663 const prevArg = segments[i - 1] ?? ""; 10664 if (this.joinExtensions.includes(this.extname(prevArg).toLowerCase())) { 10665 joined += `/../${arg}`; 10666 } else { 10667 joined += `/${arg}`; 10668 } 10669 } 10670 } 10671 } 10672 if (joined === void 0) { 10673 return "."; 10674 } 10675 return this.normalize(joined); 10676 }, 10677 /** 10678 * Returns the directory name of a path 10679 * @param path - The path to parse 10680 * @example 10681 * ```ts 10682 * // Get the directory name of a path 10683 * path.dirname('http://www.example.com/foo/bar/baz.png'); 10684 * // -> 'http://www.example.com/foo/bar' 10685 * // Get the directory name of a file path 10686 * path.dirname('C:/Users/User/Documents/file.txt'); 10687 * // -> 'C:/Users/User/Documents' 10688 * ``` 10689 */ 10690 dirname(path2) { 10691 assertPath(path2); 10692 if (path2.length === 0) 10693 return "."; 10694 path2 = this.toPosix(path2); 10695 let code = path2.charCodeAt(0); 10696 const hasRoot = code === 47; 10697 let end = -1; 10698 let matchedSlash = true; 10699 const proto = this.getProtocol(path2); 10700 const origpath = path2; 10701 path2 = path2.slice(proto.length); 10702 for (let i = path2.length - 1; i >= 1; --i) { 10703 code = path2.charCodeAt(i); 10704 if (code === 47) { 10705 if (!matchedSlash) { 10706 end = i; 10707 break; 10708 } 10709 } else { 10710 matchedSlash = false; 10711 } 10712 } 10713 if (end === -1) 10714 return hasRoot ? "/" : this.isUrl(origpath) ? proto + path2 : proto; 10715 if (hasRoot && end === 1) 10716 return "//"; 10717 return proto + path2.slice(0, end); 10718 }, 10719 /** 10720 * Returns the root of the path e.g. /, C:/, file:///, http://domain.com/ 10721 * @param path - The path to parse 10722 * @example 10723 * ```ts 10724 * // Get the root of a URL 10725 * path.rootname('http://www.example.com/foo/bar/baz.png'); 10726 * // -> 'http://www.example.com/' 10727 * // Get the root of a file path 10728 * path.rootname('C:/Users/User/Documents/file.txt'); 10729 * // -> 'C:/' 10730 * ``` 10731 */ 10732 rootname(path2) { 10733 assertPath(path2); 10734 path2 = this.toPosix(path2); 10735 let root = ""; 10736 if (path2.startsWith("/")) 10737 root = "/"; 10738 else { 10739 root = this.getProtocol(path2); 10740 } 10741 if (this.isUrl(path2)) { 10742 const index = path2.indexOf("/", root.length); 10743 if (index !== -1) { 10744 root = path2.slice(0, index); 10745 } else 10746 root = path2; 10747 if (!root.endsWith("/")) 10748 root += "/"; 10749 } 10750 return root; 10751 }, 10752 /** 10753 * Returns the last portion of a path 10754 * @param path - The path to test 10755 * @param ext - Optional extension to remove 10756 * @example 10757 * ```ts
10758 * // Get the basename of a URL 10759 * path.basename('http://www.example.com/foo/bar/baz.png'); 10760 * // -> 'baz.png' 10761 * // Get the basename of a file path 10762 * path.basename('C:/Users/User/Documents/file.txt'); 10763 * // -> 'file.txt' 10764 * ``` 10765 */ 10766 basename(path2, ext) { 10767 assertPath(path2); 10768 if (ext) 10769 assertPath(ext); 10770 path2 = removeUrlParams(this.toPosix(path2)); 10771 let start = 0; 10772 let end = -1; 10773 let matchedSlash = true; 10774 let i; 10775 if (ext !== void 0 && ext.length > 0 && ext.length <= path2.length) { 10776 if (ext.length === path2.length && ext === path2) 10777 return ""; 10778 let extIdx = ext.length - 1; 10779 let firstNonSlashEnd = -1; 10780 for (i = path2.length - 1; i >= 0; --i) { 10781 const code = path2.charCodeAt(i); 10782 if (code === 47) { 10783 if (!matchedSlash) { 10784 start = i + 1; 10785 break; 10786 } 10787 } else { 10788 if (firstNonSlashEnd === -1) { 10789 matchedSlash = false; 10790 firstNonSlashEnd = i + 1; 10791 } 10792 if (extIdx >= 0) { 10793 if (code === ext.charCodeAt(extIdx)) { 10794 if (--extIdx === -1) { 10795 end = i; 10796 } 10797 } else { 10798 extIdx = -1; 10799 end = firstNonSlashEnd; 10800 } 10801 } 10802 } 10803 } 10804 if (start === end) 10805 end = firstNonSlashEnd; 10806 else if (end === -1) 10807 end = path2.length; 10808 return path2.slice(start, end); 10809 } 10810 for (i = path2.length - 1; i >= 0; --i) { 10811 if (path2.charCodeAt(i) === 47) { 10812 if (!matchedSlash) { 10813 start = i + 1; 10814 break; 10815 } 10816 } else if (end === -1) { 10817 matchedSlash = false; 10818 end = i + 1; 10819 } 10820 } 10821 if (end === -1) 10822 return ""; 10823 return path2.slice(start, end); 10824 }, 10825 /** 10826 * Returns the extension of the path, from the last occurrence of the . (period) character to end of string in the last 10827 * portion of the path. If there is no . in the last portion of the path, or if there are no . characters other than 10828 * the first character of the basename of path, an empty string is returned. 10829 * @param path - The path to parse 10830 * @example 10831 * ```ts 10832 * // Get the extension of a URL 10833 * path.extname('http://www.example.com/foo/bar/baz.png'); 10834 * // -> '.png' 10835 * // Get the extension of a file path 10836 * path.extname('C:/Users/User/Documents/file.txt'); 10837 * // -> '.txt' 10838 * ``` 10839 */ 10840 extname(path2) { 10841 assertPath(path2); 10842 path2 = removeUrlParams(this.toPosix(path2)); 10843 let startDot = -1; 10844 let startPart = 0; 10845 let end = -1; 10846 let matchedSlash = true; 10847 let preDotState = 0; 10848 for (let i = path2.length - 1; i >= 0; --i) { 10849 const code = path2.charCodeAt(i); 10850 if (code === 47) { 10851 if (!matchedSlash) { 10852 startPart = i + 1; 10853 break; 10854 } 10855 continue; 10856 } 10857 if (end === -1) { 10858 matchedSlash = false; 10859 end = i + 1; 10860 } 10861 if (code === 46) { 10862 if (startDot === -1) 10863 startDot = i; 10864 else if (preDotState !== 1) 10865 preDotState = 1; 10866 } else if (startDot !== -1) { 10867 preDotState = -1; 10868 } 10869 } 10870 if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { 10871 return ""; 10872 } 10873 return path2.slice(startDot, end); 10874 }, 10875 /**
10876 * Parses a path into an object containing the 'root', `dir`, `base`, `ext`, and `name` properties. 10877 * @param path - The path to parse 10878 * @example 10879 * ```ts 10880 * // Parse a URL 10881 * const parsed = path.parse('http://www.example.com/foo/bar/baz.png'); 10882 * // -> { 10883 * // root: 'http://www.example.com/', 10884 * // dir: 'http://www.example.com/foo/bar', 10885 * // base: 'baz.png', 10886 * // ext: '.png', 10887 * // name: 'baz' 10888 * // } 10889 * // Parse a file path 10890 * const parsedFile = path.parse('C:/Users/User/Documents/file.txt'); 10891 * // -> { 10892 * // root: 'C:/', 10893 * // dir: 'C:/Users/User/Documents', 10894 * // base: 'file.txt', 10895 * // ext: '.txt', 10896 * // name: 'file' 10897 * // } 10898 * ``` 10899 */ 10900 parse(path2) { 10901 assertPath(path2); 10902 const ret = { root: "", dir: "", base: "", ext: "", name: "" }; 10903 if (path2.length === 0) 10904 return ret; 10905 path2 = removeUrlParams(this.toPosix(path2)); 10906 let code = path2.charCodeAt(0); 10907 const isAbsolute = this.isAbsolute(path2); 10908 let start; 10909 ret.root = this.rootname(path2); 10910 if (isAbsolute || this.hasProtocol(path2)) { 10911 start = 1; 10912 } else { 10913 start = 0; 10914 } 10915 let startDot = -1; 10916 let startPart = 0; 10917 let end = -1; 10918 let matchedSlash = true; 10919 let i = path2.length - 1; 10920 let preDotState = 0; 10921 for (; i >= start; --i) { 10922 code = path2.charCodeAt(i); 10923 if (code === 47) { 10924 if (!matchedSlash) { 10925 startPart = i + 1; 10926 break; 10927 } 10928 continue; 10929 } 10930 if (end === -1) { 10931 matchedSlash = false; 10932 end = i + 1; 10933 } 10934 if (code === 46) { 10935 if (startDot === -1) 10936 startDot = i; 10937 else if (preDotState !== 1) 10938 preDotState = 1; 10939 } else if (startDot !== -1) { 10940 preDotState = -1; 10941 } 10942 } 10943 if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { 10944 if (end !== -1) { 10945 if (startPart === 0 && isAbsolute) 10946 ret.base = ret.name = path2.slice(1, end); 10947 else 10948 ret.base = ret.name = path2.slice(startPart, end); 10949 } 10950 } else { 10951 if (startPart === 0 && isAbsolute) { 10952 ret.name = path2.slice(1, startDot); 10953 ret.base = path2.slice(1, end); 10954 } else { 10955 ret.name = path2.slice(startPart, startDot); 10956 ret.base = path2.slice(startPart, end); 10957 } 10958 ret.ext = path2.slice(startDot, end); 10959 } 10960 ret.dir = this.dirname(path2); 10961 return ret; 10962 }, 10963 sep: "/", 10964 delimiter: ":", 10965 joinExtensions: [".html"] 10966}; 10967 10968function processX(base, ids, depth, result, tags) { 10969 const id = ids[depth]; 10970 for (let i = 0; i < id.length; i++) { 10971 const value = id[i]; 10972 if (depth < ids.length - 1) { 10973 processX(base.replace(result[depth], value), ids, depth + 1, result, tags); 10974 } else { 10975 tags.push(base.replace(result[depth], value)); 10976 } 10977 } 10978} 10979function createStringVariations(string) { 10980 const regex = /\{(.*?)\}/g; 10981 const result = string.match(regex); 10982 const tags = []; 10983 if (result) { 10984 const ids = []; 10985 result.forEach((vars) => { 10986 const split = vars.substring(1, vars.length - 1).split(","); 10987 ids.push(split); 10988 }); 10989 processX(string, ids, 0, result, tags); 10990 } else { 10991 tags.push(string); 10992 } 10993 return tags; 10994} 10995 10996const isSingleItem = (item) => !Array.isArray(item); 10997 10998class Resolver { 10999 constructor() { 11000 this._defaultBundleIdentifierOptions = { 11001 connector: "-", 11002 createBundleAssetId: (bundleId, assetId) => `${bundleId}${this._bundleIdConnector}${assetId}`, 11003 extractAssetIdFromBundle: (bundleId, assetBundleId) => assetBundleId.replace(`${bundleId}${this._bundleIdConnector}`, "") 11004 }; 11005 /** The character that is used to connect the bundleId and the assetId when generating a bundle asset id key */ 11006 this._bundleIdConnector = this._defaultBundleIdentifierOptions.connector; 11007 /** 11008 * A function that generates a bundle asset id key from a bundleId and an assetId 11009 * @param bundleId - the bundleId 11010 * @param assetId - the assetId 11011 * @returns the bundle asset id key 11012 */ 11013 this._createBundleAssetId = this._defaultBundleIdentifierOptions.createBundleAssetId; 11014 /** 11015 * A function that generates an assetId from a bundle asset id key. This is the reverse of generateBundleAssetId 11016 * @param bundleId - the bundleId 11017 * @param assetBundleId - the bundle asset id key 11018 * @returns the assetId 11019 */ 11020 this._extractAssetIdFromBundle = this._defaultBundleIdentifierOptions.extractAssetIdFromBundle; 11021 this._assetMap = {}; 11022 this._preferredOrder = []; 11023 this._parsers = []; 11024 this._resolverHash = {}; 11025 this._bundles = {}; 11026 } 11027 /** 11028 * Override how the resolver deals with generating bundle ids. 11029 * must be called before any bundles are added 11030 * @param bundleIdentifier - the bundle identifier options 11031 */ 11032 setBundleIdentifier(bundleIdentifier) { 11033 this._bundleIdConnector = bundleIdentifier.connector ?? this._bundleIdConnector; 11034 this._createBundleAssetId = bundleIdentifier.createBundleAssetId ?? this._createBundleAssetId; 11035 this._extractAssetIdFromBundle = bundleIdentifier.extractAssetIdFromBundle ?? this._extractAssetIdFromBundle; 11036 if (this._extractAssetIdFromBundle("foo", this._createBundleAssetId("foo", "bar")) !== "bar") { 11037 throw new Error("[Resolver] GenerateBundleAssetId are not working correctly"); 11038 } 11039 } 11040 /** 11041 * Let the resolver know which assets you prefer to use when resolving assets. 11042 * Multiple prefer user defined rules can be added. 11043 * @example 11044 * resolver.prefer({ 11045 * // first look for something with the correct format, and then then correct resolution 11046 * priority: ['format', 'resolution'], 11047 * params:{ 11048 * format:'webp', // prefer webp images 11049 * resolution: 2, // prefer a resolution of 2 11050 * } 11051 * }) 11052 * resolver.add('foo', ['[email protected]', '[email protected]', 'bar.webp', 'bar.png']); 11053 * resolver.resolveUrl('foo') // => '[email protected]' 11054 * @param preferOrders - the prefer options 11055 */ 11056 prefer(...preferOrders) {
11057 preferOrders.forEach((prefer) => { 11058 this._preferredOrder.push(prefer); 11059 if (!prefer.priority) { 11060 prefer.priority = Object.keys(prefer.params); 11061 } 11062 }); 11063 this._resolverHash = {}; 11064 } 11065 /** 11066 * Set the base path to prepend to all urls when resolving 11067 * @example 11068 * resolver.basePath = 'https://home.com/'; 11069 * resolver.add('foo', 'bar.ong'); 11070 * resolver.resolveUrl('foo', 'bar.png'); // => 'https://home.com/bar.png' 11071 * @param basePath - the base path to use 11072 */ 11073 set basePath(basePath) { 11074 this._basePath = basePath; 11075 } 11076 get basePath() { 11077 return this._basePath; 11078 } 11079 /** 11080 * Set the root path for root-relative URLs. By default the `basePath`'s root is used. If no `basePath` is set, then the 11081 * default value for browsers is `window.location.origin` 11082 * @example 11083 * // Application hosted on https://home.com/some-path/index.html 11084 * resolver.basePath = 'https://home.com/some-path/'; 11085 * resolver.rootPath = 'https://home.com/'; 11086 * resolver.add('foo', '/bar.png'); 11087 * resolver.resolveUrl('foo', '/bar.png'); // => 'https://home.com/bar.png' 11088 * @param rootPath - the root path to use 11089 */ 11090 set rootPath(rootPath) { 11091 this._rootPath = rootPath; 11092 } 11093 get rootPath() { 11094 return this._rootPath; 11095 } 11096 /** 11097 * All the active URL parsers that help the parser to extract information and create 11098 * an asset object-based on parsing the URL itself. 11099 * 11100 * Can be added using the extensions API 11101 * @example 11102 * resolver.add('foo', [ 11103 * { 11104 * resolution: 2, 11105 * format: 'png', 11106 * src: '[email protected]', 11107 * }, 11108 * { 11109 * resolution:1, 11110 * format:'png', 11111 * src: 'image.png', 11112 * }, 11113 * ]); 11114 * 11115 * // With a url parser the information such as resolution and file format could extracted from the url itself: 11116 * extensions.add({ 11117 * extension: ExtensionType.ResolveParser, 11118 * test: loadTextures.test, // test if url ends in an image 11119 * parse: (value: string) => 11120 * ({ 11121 * resolution: parseFloat(Resolver.RETINA_PREFIX.exec(value)?.[1] ?? '1'), 11122 * format: value.split('.').pop(), 11123 * src: value, 11124 * }), 11125 * }); 11126 * 11127 * // Now resolution and format can be extracted from the url 11128 * resolver.add('foo', [ 11129 * '[email protected]', 11130 * 'image.png', 11131 * ]); 11132 */ 11133 get parsers() { 11134 return this._parsers; 11135 } 11136 /** Used for testing, this resets the resolver to its initial state */ 11137 reset() { 11138 this.setBundleIdentifier(this._defaultBundleIdentifierOptions); 11139 this._assetMap = {}; 11140 this._preferredOrder = []; 11141 this._resolverHash = {}; 11142 this._rootPath = null; 11143 this._basePath = null; 11144 this._manifest = null; 11145 this._bundles = {}; 11146 this._defaultSearchParams = null; 11147 } 11148 /** 11149 * Sets the default URL search parameters for the URL resolver. The urls can be specified as a string or an object. 11150 * @param searchParams - the default url parameters to append when resolving urls 11151 */ 11152 setDefaultSearchParams(searchParams) { 11153 if (typeof searchParams === "string") { 11154 this._defaultSearchParams = searchParams; 11155 } else { 11156 const queryValues = searchParams; 11157 this._defaultSearchParams = Object.keys(queryValues).map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryValues[key])}`).join("&"); 11158 } 11159 } 11160 /** 11161 * Returns the aliases for a given asset 11162 * @param asset - the asset to get the aliases for 11163 */ 11164 getAlias(asset) { 11165 const { alias, src } = asset; 11166 const aliasesToUse = convertToList( 11167 alias || src, 11168 (value) => { 11169 if (typeof value === "string") 11170 return value; 11171 if (Array.isArray(value)) 11172 return value.map((v) => v?.src ?? v); 11173 if (value?.src) 11174 return value.src; 11175 return value; 11176 }, 11177 true 11178 ); 11179 return aliasesToUse; 11180 } 11181 /** 11182 * Add a manifest to the asset resolver. This is a nice way to add all the asset information in one go. 11183 * generally a manifest would be built using a tool. 11184 * @param manifest - the manifest to add to the resolver 11185 */ 11186 addManifest(manifest) { 11187 if (this._manifest) { 11188 warn("[Resolver] Manifest already exists, this will be overwritten"); 11189 } 11190 this._manifest = manifest;
11191 manifest.bundles.forEach((bundle) => { 11192 this.addBundle(bundle.name, bundle.assets); 11193 }); 11194 } 11195 /** 11196 * This adds a bundle of assets in one go so that you can resolve them as a group. 11197 * For example you could add a bundle for each screen in you pixi app 11198 * @example 11199 * resolver.addBundle('animals', [ 11200 * { alias: 'bunny', src: 'bunny.png' }, 11201 * { alias: 'chicken', src: 'chicken.png' }, 11202 * { alias: 'thumper', src: 'thumper.png' }, 11203 * ]); 11204 * // or 11205 * resolver.addBundle('animals', { 11206 * bunny: 'bunny.png', 11207 * chicken: 'chicken.png', 11208 * thumper: 'thumper.png', 11209 * }); 11210 * 11211 * const resolvedAssets = await resolver.resolveBundle('animals'); 11212 * @param bundleId - The id of the bundle to add 11213 * @param assets - A record of the asset or assets that will be chosen from when loading via the specified key 11214 */ 11215 addBundle(bundleId, assets) { 11216 const assetNames = []; 11217 let convertedAssets = assets; 11218 if (!Array.isArray(assets)) { 11219 convertedAssets = Object.entries(assets).map(([alias, src]) => { 11220 if (typeof src === "string" || Array.isArray(src)) { 11221 return { alias, src }; 11222 } 11223 return { alias, ...src }; 11224 }); 11225 } 11226 convertedAssets.forEach((asset) => { 11227 const srcs = asset.src; 11228 const aliases = asset.alias; 11229 let ids; 11230 if (typeof aliases === "string") { 11231 const bundleAssetId = this._createBundleAssetId(bundleId, aliases); 11232 assetNames.push(bundleAssetId); 11233 ids = [aliases, bundleAssetId]; 11234 } else { 11235 const bundleIds = aliases.map((name) => this._createBundleAssetId(bundleId, name)); 11236 assetNames.push(...bundleIds); 11237 ids = [...aliases, ...bundleIds]; 11238 } 11239 this.add({ 11240 ...asset, 11241 ...{ 11242 alias: ids, 11243 src: srcs 11244 } 11245 }); 11246 }); 11247 this._bundles[bundleId] = assetNames; 11248 } 11249 /** 11250 * Tells the resolver what keys are associated with witch asset. 11251 * The most important thing the resolver does 11252 * @example 11253 * // Single key, single asset: 11254 * resolver.add({alias: 'foo', src: 'bar.png'); 11255 * resolver.resolveUrl('foo') // => 'bar.png' 11256 * 11257 * // Multiple keys, single asset: 11258 * resolver.add({alias: ['foo', 'boo'], src: 'bar.png'}); 11259 * resolver.resolveUrl('foo') // => 'bar.png' 11260 * resolver.resolveUrl('boo') // => 'bar.png' 11261 * 11262 * // Multiple keys, multiple assets: 11263 * resolver.add({alias: ['foo', 'boo'], src: ['bar.png', 'bar.webp']}); 11264 * resolver.resolveUrl('foo') // => 'bar.png' 11265 * 11266 * // Add custom data attached to the resolver 11267 * Resolver.add({ 11268 * alias: 'bunnyBooBooSmooth', 11269 * src: 'bunny{png,webp}', 11270 * data: { scaleMode:SCALE_MODES.NEAREST }, // Base texture options 11271 * }); 11272 * 11273 * resolver.resolve('bunnyBooBooSmooth') // => { src: 'bunny.png', data: { scaleMode: SCALE_MODES.NEAREST } } 11274 * @param aliases - the UnresolvedAsset or array of UnresolvedAssets to add to the resolver 11275 */ 11276 add(aliases) { 11277 const assets = []; 11278 if (Array.isArray(aliases)) { 11279 assets.push(...aliases); 11280 } else { 11281 assets.push(aliases); 11282 } 11283 let keyCheck; 11284 keyCheck = (key) => { 11285 if (this.hasKey(key)) { 11286 warn(`[Resolver] already has key: ${key} overwriting`); 11287 } 11288 }; 11289 const assetArray = convertToList(assets); 11290 assetArray.forEach((asset) => { 11291 const { src } = asset; 11292 let { 11293 data, 11294 format, 11295 loadParser: userDefinedLoadParser, 11296 parser: userDefinedParser 11297 } = asset; 11298 const srcsToUse = convertToList(src).map((src2) => { 11299 if (typeof src2 === "string") { 11300 return createStringVariations(src2); 11301 } 11302 return Array.isArray(src2) ? src2 : [src2]; 11303 }); 11304 const aliasesToUse = this.getAlias(asset); 11305 Array.isArray(aliasesToUse) ? aliasesToUse.forEach(keyCheck) : keyCheck(aliasesToUse); 11306 const resolvedAssets = []; 11307 const parseUrl = (url) => { 11308 const parser = this._parsers.find((p) => p.test(url)); 11309 return { 11310 ...parser?.parse(url), 11311 src: url 11312 }; 11313 }; 11314 srcsToUse.forEach((srcs) => { 11315 srcs.forEach((src2) => { 11316 let formattedAsset = {}; 11317 if (typeof src2 !== "object") { 11318 formattedAsset = parseUrl(src2); 11319 } else { 11320 data = src2.data ?? data; 11321 format = src2.format ?? format; 11322 if (src2.loadParser || src2.parser) { 11323 userDefinedLoadParser = src2.loadParser ?? userDefinedLoadParser; 11324 userDefinedParser = src2.parser ?? userDefinedParser; 11325 } 11326 formattedAsset = { 11327 ...parseUrl(src2.src), 11328 ...src2 11329 }; 11330 } 11331 if (!aliasesToUse) { 11332 throw new Error(`[Resolver] alias is undefined for this asset: ${formattedAsset.src}`); 11333 }
vendor: 5,135 bytes, lines 11334-11498
11334 formattedAsset = this._buildResolvedAsset(formattedAsset, { 11335 aliases: aliasesToUse, 11336 data, 11337 format, 11338 loadParser: userDefinedLoadParser, 11339 parser: userDefinedParser, 11340 progressSize: asset.progressSize 11341 }); 11342 resolvedAssets.push(formattedAsset); 11343 }); 11344 }); 11345 aliasesToUse.forEach((alias) => { 11346 this._assetMap[alias] = resolvedAssets; 11347 }); 11348 }); 11349 } 11350 // TODO: this needs an overload like load did in Assets 11351 /** 11352 * If the resolver has had a manifest set via setManifest, this will return the assets urls for 11353 * a given bundleId or bundleIds. 11354 * @example 11355 * // Manifest Example 11356 * const manifest = { 11357 * bundles: [ 11358 * { 11359 * name: 'load-screen', 11360 * assets: [ 11361 * { 11362 * alias: 'background', 11363 * src: 'sunset.png', 11364 * }, 11365 * { 11366 * alias: 'bar', 11367 * src: 'load-bar.{png,webp}', 11368 * }, 11369 * ], 11370 * }, 11371 * { 11372 * name: 'game-screen', 11373 * assets: [ 11374 * { 11375 * alias: 'character', 11376 * src: 'robot.png', 11377 * }, 11378 * { 11379 * alias: 'enemy', 11380 * src: 'bad-guy.png', 11381 * }, 11382 * ], 11383 * }, 11384 * ] 11385 * }; 11386 * 11387 * resolver.setManifest(manifest); 11388 * const resolved = resolver.resolveBundle('load-screen'); 11389 * @param bundleIds - The bundle ids to resolve 11390 * @returns All the bundles assets or a hash of assets for each bundle specified 11391 */ 11392 resolveBundle(bundleIds) { 11393 const singleAsset = isSingleItem(bundleIds); 11394 bundleIds = convertToList(bundleIds); 11395 const out = {}; 11396 bundleIds.forEach((bundleId) => { 11397 const assetNames = this._bundles[bundleId]; 11398 if (assetNames) { 11399 const results = this.resolve(assetNames); 11400 const assets = {}; 11401 for (const key in results) { 11402 const asset = results[key]; 11403 assets[this._extractAssetIdFromBundle(bundleId, key)] = asset; 11404 } 11405 out[bundleId] = assets; 11406 } 11407 }); 11408 return singleAsset ? out[bundleIds[0]] : out; 11409 } 11410 /** 11411 * Does exactly what resolve does, but returns just the URL rather than the whole asset object 11412 * @param key - The key or keys to resolve 11413 * @returns - The URLs associated with the key(s) 11414 */ 11415 resolveUrl(key) { 11416 const result = this.resolve(key); 11417 if (typeof key !== "string") { 11418 const out = {}; 11419 for (const i in result) { 11420 out[i] = result[i].src; 11421 } 11422 return out; 11423 } 11424 return result.src; 11425 } 11426 resolve(keys) { 11427 const singleAsset = isSingleItem(keys); 11428 keys = convertToList(keys); 11429 const result = {}; 11430 keys.forEach((key) => { 11431 if (!this._resolverHash[key]) { 11432 if (this._assetMap[key]) { 11433 let assets = this._assetMap[key]; 11434 const preferredOrder = this._getPreferredOrder(assets); 11435 preferredOrder?.priority.forEach((priorityKey) => { 11436 preferredOrder.params[priorityKey].forEach((value) => { 11437 const filteredAssets = assets.filter((asset) => { 11438 if (asset[priorityKey]) { 11439 return asset[priorityKey] === value; 11440 } 11441 return false; 11442 }); 11443 if (filteredAssets.length) { 11444 assets = filteredAssets; 11445 } 11446 }); 11447 }); 11448 this._resolverHash[key] = assets[0]; 11449 } else { 11450 this._resolverHash[key] = this._buildResolvedAsset({ 11451 alias: [key], 11452 src: key 11453 }, {}); 11454 } 11455 } 11456 result[key] = this._resolverHash[key]; 11457 }); 11458 return singleAsset ? result[keys[0]] : result; 11459 } 11460 /** 11461 * Checks if an asset with a given key exists in the resolver 11462 * @param key - The key of the asset 11463 */ 11464 hasKey(key) { 11465 return !!this._assetMap[key]; 11466 } 11467 /** 11468 * Checks if a bundle with the given key exists in the resolver 11469 * @param key - The key of the bundle 11470 */ 11471 hasBundle(key) { 11472 return !!this._bundles[key]; 11473 } 11474 /** 11475 * Internal function for figuring out what prefer criteria an asset should use. 11476 * @param assets 11477 */ 11478 _getPreferredOrder(assets) { 11479 for (let i = 0; i < assets.length; i++) { 11480 const asset = assets[i]; 11481 const preferred = this._preferredOrder.find((preference) => preference.params.format.includes(asset.format)); 11482 if (preferred) { 11483 return preferred; 11484 } 11485 } 11486 return this._preferredOrder[0]; 11487 } 11488 /** 11489 * Appends the default url parameters to the url 11490 * @param url - The url to append the default parameters to 11491 * @returns - The url with the default parameters appended 11492 */ 11493 _appendDefaultSearchParams(url) { 11494 if (!this._defaultSearchParams) 11495 return url; 11496 const paramConnector = /\?/.test(url) ? "&" : "?"; 11497 return `${url}${paramConnector}${this._defaultSearchParams}`; 11498 }
11499 _buildResolvedAsset(formattedAsset, data) { 11500 const { aliases, data: assetData, loadParser, parser, format, progressSize } = data; 11501 if (this._basePath || this._rootPath) { 11502 formattedAsset.src = path.toAbsolute(formattedAsset.src, this._basePath, this._rootPath); 11503 } 11504 formattedAsset.alias = aliases ?? formattedAsset.alias ?? [formattedAsset.src]; 11505 formattedAsset.src = this._appendDefaultSearchParams(formattedAsset.src); 11506 formattedAsset.data = { ...assetData || {}, ...formattedAsset.data }; 11507 formattedAsset.loadParser = loadParser ?? formattedAsset.loadParser; 11508 formattedAsset.parser = parser ?? formattedAsset.parser; 11509 formattedAsset.format = format ?? formattedAsset.format ?? getUrlExtension(formattedAsset.src); 11510 if (progressSize !== void 0) { 11511 formattedAsset.progressSize = progressSize; 11512 } 11513 return formattedAsset; 11514 } 11515} 11516/** 11517 * The prefix that denotes a URL is for a retina asset. 11518 * @default /@([0-9\.]+)x/ 11519 * @example `@2x` 11520 */ 11521Resolver.RETINA_PREFIX = /@([0-9\.]+)x/; 11522function getUrlExtension(url) { 11523 return url.split(".").pop().split("?").shift().split("#").shift(); 11524} 11525 11526const copySearchParams = (targetUrl, sourceUrl) => { 11527 const searchParams = sourceUrl.split("?")[1]; 11528 if (searchParams) { 11529 targetUrl += `?${searchParams}`; 11530 } 11531 return targetUrl; 11532}; 11533 11534const _Spritesheet = class _Spritesheet { 11535 constructor(optionsOrTexture, arg1) { 11536 /** For multi-packed spritesheets, this contains a reference to all the other spritesheets it depends on. */ 11537 this.linkedSheets = []; 11538 let options = optionsOrTexture; 11539 if (optionsOrTexture?.source instanceof TextureSource) { 11540 options = { 11541 texture: optionsOrTexture, 11542 data: arg1 11543 }; 11544 } 11545 const { texture, data, cachePrefix = "" } = options; 11546 this.cachePrefix = cachePrefix; 11547 this._texture = texture instanceof Texture ? texture : null; 11548 this.textureSource = texture.source; 11549 this.textures = {}; 11550 this.animations = {}; 11551 this.data = data; 11552 const metaResolution = parseFloat(data.meta.scale); 11553 if (metaResolution) { 11554 this.resolution = metaResolution; 11555 texture.source.resolution = this.resolution; 11556 } else { 11557 this.resolution = texture.source._resolution; 11558 } 11559 this._frames = this.data.frames; 11560 this._frameKeys = Object.keys(this._frames); 11561 this._batchIndex = 0; 11562 this._callback = null; 11563 } 11564 /** 11565 * Parser spritesheet from loaded data. This is done asynchronously 11566 * to prevent creating too many Texture within a single process. 11567 */ 11568 parse() { 11569 return new Promise((resolve) => { 11570 this._callback = resolve; 11571 this._batchIndex = 0; 11572 if (this._frameKeys.length <= _Spritesheet.BATCH_SIZE) { 11573 this._processFrames(0); 11574 this._processAnimations(); 11575 this._parseComplete(); 11576 } else { 11577 this._nextBatch(); 11578 } 11579 }); 11580 } 11581 /** 11582 * Process a batch of frames 11583 * @param initialFrameIndex - The index of frame to start. 11584 */ 11585 _processFrames(initialFrameIndex) { 11586 let frameIndex = initialFrameIndex; 11587 const maxFrames = _Spritesheet.BATCH_SIZE; 11588 while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) { 11589 const i = this._frameKeys[frameIndex]; 11590 const data = this._frames[i]; 11591 const rect = data.frame; 11592 if (rect) { 11593 let frame = null; 11594 let trim = null; 11595 const sourceSize = data.trimmed !== false && data.sourceSize ? data.sourceSize : data.frame; 11596 const orig = new Rectangle( 11597 0, 11598 0, 11599 Math.floor(sourceSize.w) / this.resolution, 11600 Math.floor(sourceSize.h) / this.resolution 11601 ); 11602 if (data.rotated) { 11603 frame = new Rectangle( 11604 Math.floor(rect.x) / this.resolution, 11605 Math.floor(rect.y) / this.resolution, 11606 Math.floor(rect.h) / this.resolution, 11607 Math.floor(rect.w) / this.resolution 11608 ); 11609 } else { 11610 frame = new Rectangle( 11611 Math.floor(rect.x) / this.resolution, 11612 Math.floor(rect.y) / this.resolution, 11613 Math.floor(rect.w) / this.resolution, 11614 Math.floor(rect.h) / this.resolution 11615 ); 11616 } 11617 if (data.trimmed !== false && data.spriteSourceSize) { 11618 trim = new Rectangle( 11619 Math.floor(data.spriteSourceSize.x) / this.resolution, 11620 Math.floor(data.spriteSourceSize.y) / this.resolution, 11621 Math.floor(rect.w) / this.resolution, 11622 Math.floor(rect.h) / this.resolution 11623 ); 11624 } 11625 this.textures[i] = new Texture({ 11626 source: this.textureSource, 11627 frame, 11628 orig, 11629 trim, 11630 rotate: data.rotated ? 2 : 0, 11631 defaultAnchor: data.anchor, 11632 defaultBorders: data.borders, 11633 label: i.toString() 11634 }); 11635 } 11636 frameIndex++; 11637 } 11638 } 11639 /** Parse animations config. */ 11640 _processAnimations() { 11641 const animations = this.data.animations || {}; 11642 for (const animName in animations) { 11643 this.animations[animName] = []; 11644 for (let i = 0; i < animations[animName].length; i++) { 11645 const frameName = animations[animName][i]; 11646 this.animations[animName].push(this.textures[frameName]); 11647 } 11648 } 11649 } 11650 /** The parse has completed. */ 11651 _parseComplete() { 11652 const callback = this._callback; 11653 this._callback = null; 11654 this._batchIndex = 0; 11655 callback.call(this, this.textures); 11656 } 11657 /** Begin the next batch of textures. */ 11658 _nextBatch() { 11659 this._processFrames(this._batchIndex * _Spritesheet.BATCH_SIZE); 11660 this._batchIndex++; 11661 setTimeout(() => { 11662 if (this._batchIndex * _Spritesheet.BATCH_SIZE < this._frameKeys.length) { 11663 this._nextBatch(); 11664 } else { 11665 this._processAnimations(); 11666 this._parseComplete(); 11667 } 11668 }, 0); 11669 } 11670 /** 11671 * Destroy Spritesheet and don't use after this. 11672 * @param {boolean} [destroyBase=false] - Whether to destroy the base texture as well 11673 */ 11674 destroy(destroyBase = false) { 11675 for (const i in this.textures) { 11676 this.textures[i].destroy(); 11677 } 11678 this._frames = null; 11679 this._frameKeys = null; 11680 this.data = null; 11681 this.textures = null; 11682 if (destroyBase) { 11683 this._texture?.destroy(); 11684 this.textureSource.destroy(); 11685 } 11686 this._texture = null; 11687 this.textureSource = null; 11688 this.linkedSheets = []; 11689 } 11690}; 11691/** 11692 * The maximum number of Textures to build per process. 11693 * @advanced 11694 */ 11695_Spritesheet.BATCH_SIZE = 1e3; 11696let Spritesheet = _Spritesheet; 11697 11698const validImages = [ 11699 "jpg", 11700 "png", 11701 "jpeg", 11702 "avif", 11703 "webp", 11704 "basis", 11705 "etc2", 11706 "bc7", 11707 "bc6h", 11708 "bc5", 11709 "bc4", 11710 "bc3", 11711 "bc2", 11712 "bc1", 11713 "eac", 11714 "astc" 11715]; 11716function getCacheableAssets(keys, asset, ignoreMultiPack) { 11717 const out = {}; 11718 keys.forEach((key) => { 11719 out[key] = asset; 11720 });
11721 Object.keys(asset.textures).forEach((key) => { 11722 out[`${asset.cachePrefix}${key}`] = asset.textures[key]; 11723 }); 11724 if (!ignoreMultiPack) { 11725 const basePath = path.dirname(keys[0]); 11726 asset.linkedSheets.forEach((item, i) => { 11727 const out2 = getCacheableAssets([`${basePath}/${asset.data.meta.related_multi_packs[i]}`], item, true); 11728 Object.assign(out, out2); 11729 }); 11730 } 11731 return out; 11732} 11733const spritesheetAsset = { 11734 extension: ExtensionType.Asset, 11735 /** Handle the caching of the related Spritesheet Textures */ 11736 cache: { 11737 test: (asset) => asset instanceof Spritesheet, 11738 getCacheableAssets: (keys, asset) => getCacheableAssets(keys, asset, false) 11739 }, 11740 /** Resolve the resolution of the asset. */ 11741 resolver: { 11742 extension: { 11743 type: ExtensionType.ResolveParser, 11744 name: "resolveSpritesheet" 11745 }, 11746 test: (value) => { 11747 const tempURL = value.split("?")[0]; 11748 const split = tempURL.split("."); 11749 const extension = split.pop(); 11750 const format = split.pop(); 11751 return extension === "json" && validImages.includes(format); 11752 }, 11753 parse: (value) => { 11754 const split = value.split("."); 11755 return { 11756 resolution: parseFloat(Resolver.RETINA_PREFIX.exec(value)?.[1] ?? "1"), 11757 format: split[split.length - 2], 11758 src: value 11759 }; 11760 } 11761 }, 11762 /** 11763 * Loader plugin that parses sprite sheets! 11764 * once the JSON has been loaded this checks to see if the JSON is spritesheet data. 11765 * If it is, we load the spritesheets image and parse the data into Spritesheet 11766 * All textures in the sprite sheet are then added to the cache 11767 */ 11768 loader: { 11769 /** used for deprecation purposes */ 11770 name: "spritesheetLoader", 11771 id: "spritesheet", 11772 extension: { 11773 type: ExtensionType.LoadParser, 11774 priority: LoaderParserPriority.Normal, 11775 name: "spritesheetLoader" 11776 }, 11777 async testParse(asset, options) { 11778 return path.extname(options.src).toLowerCase() === ".json" && !!asset.frames; 11779 }, 11780 async parse(asset, options, loader) { 11781 const { 11782 texture: imageTexture, 11783 // if user need to use preloaded texture 11784 imageFilename, 11785 // if user need to use custom filename (not from jsonFile.meta.image) 11786 textureOptions, 11787 // if user need to set texture options on texture 11788 cachePrefix 11789 // if user need to use custom cache prefix 11790 } = options?.data ?? {}; 11791 let basePath = path.dirname(options.src); 11792 if (basePath && basePath.lastIndexOf("/") !== basePath.length - 1) { 11793 basePath += "/"; 11794 } 11795 let texture; 11796 if (imageTexture instanceof Texture) { 11797 texture = imageTexture; 11798 } else { 11799 const imagePath = copySearchParams(basePath + (imageFilename ?? asset.meta.image), options.src); 11800 const assets = await loader.load([{ src: imagePath, data: textureOptions }]); 11801 texture = assets[imagePath]; 11802 } 11803 const spritesheet = new Spritesheet({ 11804 texture: texture.source, 11805 data: asset, 11806 cachePrefix 11807 }); 11808 await spritesheet.parse(); 11809 const multiPacks = asset?.meta?.related_multi_packs; 11810 if (Array.isArray(multiPacks)) { 11811 const promises = []; 11812 for (const item of multiPacks) { 11813 if (typeof item !== "string") { 11814 continue; 11815 } 11816 let itemUrl = basePath + item; 11817 if (options.data?.ignoreMultiPack) { 11818 continue; 11819 } 11820 itemUrl = copySearchParams(itemUrl, options.src); 11821 promises.push(loader.load({ 11822 src: itemUrl, 11823 data: { 11824 textureOptions, 11825 ignoreMultiPack: true 11826 } 11827 })); 11828 } 11829 const res = await Promise.all(promises); 11830 spritesheet.linkedSheets = res; 11831 res.forEach((item) => { 11832 item.linkedSheets = [spritesheet].concat(spritesheet.linkedSheets.filter((sp) => sp !== item)); 11833 }); 11834 } 11835 return spritesheet; 11836 }, 11837 async unload(spritesheet, _resolvedAsset, loader) { 11838 await loader.unload(spritesheet.textureSource._sourceOrigin); 11839 spritesheet.destroy(false); 11840 } 11841 } 11842}; 11843 11844extensions.add(spritesheetAsset); 11845 11846const idCounts = /* @__PURE__ */ Object.create(null); 11847const idHash = /* @__PURE__ */ Object.create(null); 11848function createIdFromString(value, groupId) { 11849 let id = idHash[value]; 11850 if (id === void 0) { 11851 if (idCounts[groupId] === void 0) { 11852 idCounts[groupId] = 1; 11853 } 11854 idHash[value] = id = idCounts[groupId]++; 11855 } 11856 return id; 11857} 11858 11859let context; 11860function getTestContext() { 11861 if (!context || context?.isContextLost()) { 11862 const canvas = DOMAdapter.get().createCanvas(); 11863 context = canvas.getContext("webgl", {}); 11864 } 11865 return context; 11866} 11867 11868let maxFragmentPrecision; 11869function getMaxFragmentPrecision() { 11870 if (!maxFragmentPrecision) { 11871 maxFragmentPrecision = "mediump"; 11872 const gl = getTestContext(); 11873 if (gl) { 11874 if (gl.getShaderPrecisionFormat) { 11875 const shaderFragment = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); 11876 maxFragmentPrecision = shaderFragment.precision ? "highp" : "mediump"; 11877 } 11878 } 11879 } 11880 return maxFragmentPrecision; 11881} 11882 11883function addProgramDefines(src, isES300, isFragment) { 11884 if (isES300) 11885 return src; 11886 if (isFragment) { 11887 src = src.replace("out vec4 finalColor;", ""); 11888 return ` 11889 11890 #ifdef GL_ES // This checks if it is WebGL1 11891 #define in varying 11892 #define finalColor gl_FragColor 11893 #define texture texture2D 11894 #endif 11895 ${src} 11896 `; 11897 } 11898 return ` 11899 11900 #ifdef GL_ES // This checks if it is WebGL1 11901 #define in attribute 11902 #define out varying 11903 #endif 11904 ${src} 11905 `; 11906} 11907 11908function ensurePrecision(src, options, isFragment) { 11909 const maxSupportedPrecision = isFragment ? options.maxSupportedFragmentPrecision : options.maxSupportedVertexPrecision; 11910 if (src.substring(0, 9) !== "precision") { 11911 let precision = isFragment ? options.requestedFragmentPrecision : options.requestedVertexPrecision; 11912 if (precision === "highp" && maxSupportedPrecision !== "highp") { 11913 precision = "mediump"; 11914 } 11915 return `precision ${precision} float; 11916${src}`; 11917 } else if (maxSupportedPrecision !== "highp" && src.substring(0, 15) === "precision highp") { 11918 return src.replace("precision highp", "precision mediump"); 11919 } 11920 return src; 11921} 11922 11923function insertVersion(src, isES300) { 11924 if (!isES300) 11925 return src; 11926 return `#version 300 es 11927${src}`; 11928} 11929 11930const fragmentNameCache = {}; 11931const VertexNameCache = {}; 11932function setProgramName(src, { name = `pixi-program` }, isFragment = true) { 11933 name = name.replace(/\s+/g, "-"); 11934 name += isFragment ? "-fragment" : "-vertex"; 11935 const nameCache = isFragment ? fragmentNameCache : VertexNameCache; 11936 if (nameCache[name]) { 11937 nameCache[name]++; 11938 name += `-${nameCache[name]}`;
11939 } else { 11940 nameCache[name] = 1; 11941 } 11942 if (src.indexOf("#define SHADER_NAME") !== -1) 11943 return src; 11944 const shaderName = `#define SHADER_NAME ${name}`; 11945 return `${shaderName} 11946${src}`; 11947} 11948 11949function stripVersion(src, isES300) { 11950 if (!isES300) 11951 return src; 11952 return src.replace("#version 300 es", ""); 11953} 11954 11955const processes = { 11956 // strips any version headers.. 11957 stripVersion, 11958 // adds precision string if not already present 11959 ensurePrecision, 11960 // add some defines if WebGL1 to make it more compatible with WebGL2 shaders 11961 addProgramDefines, 11962 // add the program name to the shader 11963 setProgramName, 11964 // add the version string to the shader header 11965 insertVersion 11966}; 11967const programCache$1 = /* @__PURE__ */ Object.create(null); 11968const _GlProgram = class _GlProgram { 11969 /** 11970 * Creates a shiny new GlProgram. Used by WebGL renderer. 11971 * @param options - The options for the program. 11972 */ 11973 constructor(options) { 11974 options = { ..._GlProgram.defaultOptions, ...options }; 11975 const isES300 = options.fragment.indexOf("#version 300 es") !== -1; 11976 const preprocessorOptions = { 11977 stripVersion: isES300, 11978 ensurePrecision: { 11979 requestedFragmentPrecision: options.preferredFragmentPrecision, 11980 requestedVertexPrecision: options.preferredVertexPrecision, 11981 maxSupportedVertexPrecision: "highp", 11982 maxSupportedFragmentPrecision: getMaxFragmentPrecision() 11983 }, 11984 setProgramName: { 11985 name: options.name 11986 }, 11987 addProgramDefines: isES300, 11988 insertVersion: isES300 11989 }; 11990 let fragment = options.fragment; 11991 let vertex = options.vertex; 11992 Object.keys(processes).forEach((processKey) => { 11993 const processOptions = preprocessorOptions[processKey]; 11994 fragment = processes[processKey](fragment, processOptions, true); 11995 vertex = processes[processKey](vertex, processOptions, false); 11996 }); 11997 this.fragment = fragment; 11998 this.vertex = vertex; 11999 this.transformFeedbackVaryings = options.transformFeedbackVaryings; 12000 this._key = createIdFromString(`${this.vertex}:${this.fragment}`, "gl-program"); 12001 } 12002 /** destroys the program */ 12003 destroy() { 12004 this.fragment = null; 12005 this.vertex = null; 12006 this._attributeData = null; 12007 this._uniformData = null; 12008 this._uniformBlockData = null; 12009 this.transformFeedbackVaryings = null; 12010 programCache$1[this._cacheKey] = null; 12011 } 12012 /** 12013 * Helper function that creates a program for a given source. 12014 * It will check the program cache if the program has already been created. 12015 * If it has that one will be returned, if not a new one will be created and cached. 12016 * @param options - The options for the program. 12017 * @returns A program using the same source 12018 */ 12019 static from(options) { 12020 const key = `${options.vertex}:${options.fragment}`; 12021 if (!programCache$1[key]) { 12022 programCache$1[key] = new _GlProgram(options); 12023 programCache$1[key]._cacheKey = key; 12024 } 12025 return programCache$1[key]; 12026 } 12027}; 12028/** The default options used by the program. */ 12029_GlProgram.defaultOptions = { 12030 preferredVertexPrecision: "highp", 12031 preferredFragmentPrecision: "mediump" 12032}; 12033let GlProgram = _GlProgram; 12034 12035const attributeFormatData = { 12036 uint8x2: { size: 2, stride: 2, normalised: false }, 12037 uint8x4: { size: 4, stride: 4, normalised: false }, 12038 sint8x2: { size: 2, stride: 2, normalised: false }, 12039 sint8x4: { size: 4, stride: 4, normalised: false }, 12040 unorm8x2: { size: 2, stride: 2, normalised: true }, 12041 unorm8x4: { size: 4, stride: 4, normalised: true }, 12042 snorm8x2: { size: 2, stride: 2, normalised: true }, 12043 snorm8x4: { size: 4, stride: 4, normalised: true }, 12044 uint16x2: { size: 2, stride: 4, normalised: false }, 12045 uint16x4: { size: 4, stride: 8, normalised: false }, 12046 sint16x2: { size: 2, stride: 4, normalised: false }, 12047 sint16x4: { size: 4, stride: 8, normalised: false }, 12048 unorm16x2: { size: 2, stride: 4, normalised: true }, 12049 unorm16x4: { size: 4, stride: 8, normalised: true }, 12050 snorm16x2: { size: 2, stride: 4, normalised: true }, 12051 snorm16x4: { size: 4, stride: 8, normalised: true },
12052 float16x2: { size: 2, stride: 4, normalised: false }, 12053 float16x4: { size: 4, stride: 8, normalised: false }, 12054 float32: { size: 1, stride: 4, normalised: false }, 12055 float32x2: { size: 2, stride: 8, normalised: false }, 12056 float32x3: { size: 3, stride: 12, normalised: false }, 12057 float32x4: { size: 4, stride: 16, normalised: false }, 12058 uint32: { size: 1, stride: 4, normalised: false }, 12059 uint32x2: { size: 2, stride: 8, normalised: false }, 12060 uint32x3: { size: 3, stride: 12, normalised: false }, 12061 uint32x4: { size: 4, stride: 16, normalised: false }, 12062 sint32: { size: 1, stride: 4, normalised: false }, 12063 sint32x2: { size: 2, stride: 8, normalised: false }, 12064 sint32x3: { size: 3, stride: 12, normalised: false }, 12065 sint32x4: { size: 4, stride: 16, normalised: false } 12066}; 12067function getAttributeInfoFromFormat(format) { 12068 return attributeFormatData[format] ?? attributeFormatData.float32; 12069} 12070 12071const WGSL_TO_VERTEX_TYPES = { 12072 f32: "float32", 12073 "vec2<f32>": "float32x2", 12074 "vec3<f32>": "float32x3", 12075 "vec4<f32>": "float32x4", 12076 vec2f: "float32x2", 12077 vec3f: "float32x3", 12078 vec4f: "float32x4", 12079 i32: "sint32", 12080 "vec2<i32>": "sint32x2", 12081 "vec3<i32>": "sint32x3", 12082 "vec4<i32>": "sint32x4", 12083 u32: "uint32", 12084 "vec2<u32>": "uint32x2", 12085 "vec3<u32>": "uint32x3", 12086 "vec4<u32>": "uint32x4", 12087 bool: "uint32", 12088 "vec2<bool>": "uint32x2", 12089 "vec3<bool>": "uint32x3", 12090 "vec4<bool>": "uint32x4" 12091}; 12092function extractAttributesFromGpuProgram({ source, entryPoint }) { 12093 const results = {}; 12094 const mainVertStart = source.indexOf(`fn ${entryPoint}`); 12095 if (mainVertStart !== -1) { 12096 const arrowFunctionStart = source.indexOf("->", mainVertStart); 12097 if (arrowFunctionStart !== -1) { 12098 const functionArgsSubstring = source.substring(mainVertStart, arrowFunctionStart); 12099 const inputsRegex = /@location\((\d+)\)\s+([a-zA-Z0-9_]+)\s*:\s*([a-zA-Z0-9_<>]+)(?:,|\s|$)/g; 12100 let match; 12101 while ((match = inputsRegex.exec(functionArgsSubstring)) !== null) { 12102 const format = WGSL_TO_VERTEX_TYPES[match[3]] ?? "float32"; 12103 results[match[2]] = { 12104 location: parseInt(match[1], 10), 12105 format, 12106 stride: getAttributeInfoFromFormat(format).stride, 12107 offset: 0, 12108 instance: false, 12109 start: 0 12110 }; 12111 } 12112 } 12113 } 12114 return results; 12115} 12116 12117function extractStructAndGroups(wgsl) { 12118 const linePattern = /(^|[^/])@(group|binding)\(\d+\)[^;]+;/g; 12119 const groupPattern = /@group\((\d+)\)/; 12120 const bindingPattern = /@binding\((\d+)\)/; 12121 const namePattern = /var(<[^>]+>)? (\w+)/; 12122 const typePattern = /:\s*(\w+)/; 12123 const structPattern = /struct\s+(\w+)\s*{([^}]+)}/g; 12124 const structMemberPattern = /(\w+)\s*:\s*([\w\<\>]+)/g; 12125 const structName = /struct\s+(\w+)/; 12126 const groups = wgsl.match(linePattern)?.map((item) => ({ 12127 group: parseInt(item.match(groupPattern)[1], 10), 12128 binding: parseInt(item.match(bindingPattern)[1], 10), 12129 name: item.match(namePattern)[2], 12130 isUniform: item.match(namePattern)[1] === "<uniform>", 12131 type: item.match(typePattern)[1] 12132 })); 12133 if (!groups) { 12134 return { 12135 groups: [], 12136 structs: [] 12137 }; 12138 } 12139 const structs = wgsl.match(structPattern)?.map((struct) => { 12140 const name = struct.match(structName)[1]; 12141 const members = struct.match(structMemberPattern).reduce((acc, member) => { 12142 const [name2, type] = member.split(":"); 12143 acc[name2.trim()] = type.trim(); 12144 return acc; 12145 }, {}); 12146 if (!members) { 12147 return null; 12148 } 12149 return { name, members }; 12150 }).filter(({ name }) => groups.some((group) => group.type === name)) ?? []; 12151 return { 12152 groups, 12153 structs 12154 }; 12155} 12156 12157var ShaderStage = /* @__PURE__ */ ((ShaderStage2) => { 12158 ShaderStage2[ShaderStage2["VERTEX"] = 1] = "VERTEX"; 12159 ShaderStage2[ShaderStage2["FRAGMENT"] = 2] = "FRAGMENT"; 12160 ShaderStage2[ShaderStage2["COMPUTE"] = 4] = "COMPUTE"; 12161 return ShaderStage2; 12162})(ShaderStage || {}); 12163 12164function generateGpuLayoutGroups({ groups }) { 12165 const layout = []; 12166 for (let i = 0; i < groups.length; i++) { 12167 const group = groups[i]; 12168 if (!layout[group.group]) { 12169 layout[group.group] = []; 12170 } 12171 if (group.isUniform) { 12172 layout[group.group].push({ 12173 binding: group.binding, 12174 visibility: ShaderStage.VERTEX | ShaderStage.FRAGMENT, 12175 buffer: { 12176 type: "uniform" 12177 } 12178 }); 12179 } else if (group.type === "sampler") { 12180 layout[group.group].push({ 12181 binding: group.binding, 12182 visibility: ShaderStage.FRAGMENT, 12183 sampler: { 12184 type: "filtering" 12185 } 12186 }); 12187 } else if (group.type === "texture_2d") { 12188 layout[group.group].push({ 12189 binding: group.binding, 12190 visibility: ShaderStage.FRAGMENT, 12191 texture: { 12192 sampleType: "float", 12193 viewDimension: "2d", 12194 multisampled: false 12195 } 12196 }); 12197 } 12198 } 12199 return layout; 12200} 12201 12202function generateLayoutHash({ groups }) { 12203 const layout = []; 12204 for (let i = 0; i < groups.length; i++) { 12205 const group = groups[i]; 12206 if (!layout[group.group]) { 12207 layout[group.group] = {}; 12208 } 12209 layout[group.group][group.name] = group.binding; 12210 } 12211 return layout; 12212} 12213
12214function removeStructAndGroupDuplicates(vertexStructsAndGroups, fragmentStructsAndGroups) { 12215 const structNameSet = /* @__PURE__ */ new Set(); 12216 const dupeGroupKeySet = /* @__PURE__ */ new Set(); 12217 const structs = [...vertexStructsAndGroups.structs, ...fragmentStructsAndGroups.structs].filter((struct) => { 12218 if (structNameSet.has(struct.name)) { 12219 return false; 12220 } 12221 structNameSet.add(struct.name); 12222 return true; 12223 }); 12224 const groups = [...vertexStructsAndGroups.groups, ...fragmentStructsAndGroups.groups].filter((group) => { 12225 const key = `${group.name}-${group.binding}`; 12226 if (dupeGroupKeySet.has(key)) { 12227 return false; 12228 } 12229 dupeGroupKeySet.add(key); 12230 return true; 12231 }); 12232 return { structs, groups }; 12233} 12234 12235const programCache = /* @__PURE__ */ Object.create(null); 12236class GpuProgram { 12237 /** 12238 * Create a new GpuProgram 12239 * @param options - The options for the gpu program 12240 */ 12241 constructor(options) { 12242 /** @internal */ 12243 this._layoutKey = 0; 12244 /** @internal */ 12245 this._attributeLocationsKey = 0; 12246 const { fragment, vertex, layout, gpuLayout, name } = options; 12247 this.name = name; 12248 this.fragment = fragment; 12249 this.vertex = vertex; 12250 if (fragment.source === vertex.source) { 12251 const structsAndGroups = extractStructAndGroups(fragment.source); 12252 this.structsAndGroups = structsAndGroups; 12253 } else { 12254 const vertexStructsAndGroups = extractStructAndGroups(vertex.source); 12255 const fragmentStructsAndGroups = extractStructAndGroups(fragment.source); 12256 this.structsAndGroups = removeStructAndGroupDuplicates(vertexStructsAndGroups, fragmentStructsAndGroups); 12257 } 12258 this.layout = layout ?? generateLayoutHash(this.structsAndGroups); 12259 this.gpuLayout = gpuLayout ?? generateGpuLayoutGroups(this.structsAndGroups); 12260 this.autoAssignGlobalUniforms = !!(this.layout[0]?.globalUniforms !== void 0); 12261 this.autoAssignLocalUniforms = !!(this.layout[1]?.localUniforms !== void 0); 12262 this._generateProgramKey(); 12263 } 12264 // TODO maker this pure 12265 _generateProgramKey() { 12266 const { vertex, fragment } = this; 12267 const bigKey = vertex.source + fragment.source + vertex.entryPoint + fragment.entryPoint; 12268 this._layoutKey = createIdFromString(bigKey, "program"); 12269 } 12270 get attributeData() { 12271 this._attributeData ?? (this._attributeData = extractAttributesFromGpuProgram(this.vertex)); 12272 return this._attributeData; 12273 } 12274 /** destroys the program */ 12275 destroy() { 12276 this.gpuLayout = null; 12277 this.layout = null; 12278 this.structsAndGroups = null; 12279 this.fragment = null; 12280 this.vertex = null; 12281 programCache[this._cacheKey] = null; 12282 } 12283 /** 12284 * Helper function that creates a program for a given source. 12285 * It will check the program cache if the program has already been created. 12286 * If it has that one will be returned, if not a new one will be created and cached. 12287 * @param options - The options for the program. 12288 * @returns A program using the same source 12289 */ 12290 static from(options) { 12291 const key = `${options.vertex.source}:${options.fragment.source}:${options.fragment.entryPoint}:${options.vertex.entryPoint}`; 12292 if (!programCache[key]) { 12293 programCache[key] = new GpuProgram(options); 12294 programCache[key]._cacheKey = key; 12295 } 12296 return programCache[key]; 12297 } 12298} 12299 12300const UNIFORM_TYPES_VALUES = [ 12301 "f32", 12302 "i32", 12303 "vec2<f32>", 12304 "vec3<f32>", 12305 "vec4<f32>", 12306 "mat2x2<f32>", 12307 "mat3x3<f32>", 12308 "mat4x4<f32>", 12309 "mat3x2<f32>", 12310 "mat4x2<f32>", 12311 "mat2x3<f32>", 12312 "mat4x3<f32>", 12313 "mat2x4<f32>", 12314 "mat3x4<f32>", 12315 "vec2<i32>", 12316 "vec3<i32>", 12317 "vec4<i32>" 12318]; 12319const UNIFORM_TYPES_MAP = UNIFORM_TYPES_VALUES.reduce((acc, type) => { 12320 acc[type] = true; 12321 return acc; 12322}, {}); 12323 12324function getDefaultUniformValue(type, size) { 12325 switch (type) { 12326 case "f32": 12327 return 0; 12328 case "vec2<f32>": 12329 return new Float32Array(2 * size); 12330 case "vec3<f32>": 12331 return new Float32Array(3 * size); 12332 case "vec4<f32>": 12333 return new Float32Array(4 * size); 12334 case "mat2x2<f32>": 12335 return new Float32Array([ 12336 1, 12337 0, 12338 0, 12339 1 12340 ]); 12341 case "mat3x3<f32>": 12342 return new Float32Array([ 12343 1, 12344 0, 12345 0, 12346 0, 12347 1, 12348 0, 12349 0, 12350 0, 12351 1 12352 ]); 12353 case "mat4x4<f32>": 12354 return new Float32Array([ 12355 1, 12356 0, 12357 0, 12358 0, 12359 0, 12360 1, 12361 0, 12362 0, 12363 0, 12364 0, 12365 1, 12366 0, 12367 0, 12368 0, 12369 0, 12370 1 12371 ]); 12372 } 12373 return null; 12374} 12375 12376const _UniformGroup = class _UniformGroup { 12377 /** 12378 * Create a new Uniform group 12379 * @param uniformStructures - The structures of the uniform group 12380 * @param options - The optional parameters of this uniform group 12381 */ 12382 constructor(uniformStructures, options) { 12383 /** 12384 * used internally to know if a uniform group was used in the last render pass 12385 * @internal 12386 */ 12387 this._touched = 0; 12388 /** a unique id for this uniform group used through the renderer */ 12389 this.uid = uid("uniform"); 12390 /** 12391 * a resource type, used to identify how to handle it when its in a bind group / shader resource 12392 * @internal 12393 */ 12394 this._resourceType = "uniformGroup"; 12395 /** 12396 * the resource id used internally by the renderer to build bind group keys 12397 * @internal 12398 */ 12399 this._resourceId = uid("resource"); 12400 /** used ito identify if this is a uniform group */ 12401 this.isUniformGroup = true; 12402 /** 12403 * used to flag if this Uniform groups data is different from what it has stored in its buffer / on the GPU 12404 * @internal 12405 */ 12406 this._dirtyId = 0; 12407 // implementing the interface - UniformGroup are not destroyed 12408 this.destroyed = false;
12409 options = { ..._UniformGroup.defaultOptions, ...options }; 12410 this.uniformStructures = uniformStructures; 12411 const uniforms = {}; 12412 for (const i in uniformStructures) { 12413 const uniformData = uniformStructures[i]; 12414 uniformData.name = i; 12415 uniformData.size = uniformData.size ?? 1; 12416 if (!UNIFORM_TYPES_MAP[uniformData.type]) { 12417 const arrayMatch = uniformData.type.match(/^array<(\w+(?:<\w+>)?),\s*(\d+)>$/); 12418 if (arrayMatch) { 12419 const [, innerType, size] = arrayMatch; 12420 throw new Error( 12421 `Uniform type ${uniformData.type} is not supported. Use type: '${innerType}', size: ${size} instead.` 12422 ); 12423 } 12424 throw new Error(`Uniform type ${uniformData.type} is not supported. Supported uniform types are: ${UNIFORM_TYPES_VALUES.join(", ")}`); 12425 } 12426 uniformData.value ?? (uniformData.value = getDefaultUniformValue(uniformData.type, uniformData.size)); 12427 uniforms[i] = uniformData.value; 12428 } 12429 this.uniforms = uniforms; 12430 this._dirtyId = 1; 12431 this.ubo = options.ubo; 12432 this.isStatic = options.isStatic; 12433 this._signature = createIdFromString(Object.keys(uniforms).map( 12434 (i) => `${i}-${uniformStructures[i].type}` 12435 ).join("-"), "uniform-group"); 12436 } 12437 /** Call this if you want the uniform groups data to be uploaded to the GPU only useful if `isStatic` is true. */ 12438 update() { 12439 this._dirtyId++; 12440 } 12441}; 12442/** The default options used by the uniform group. */ 12443_UniformGroup.defaultOptions = { 12444 /** if true the UniformGroup is handled as an Uniform buffer object. */ 12445 ubo: false, 12446 /** if true, then you are responsible for when the data is uploaded to the GPU by calling `update()` */ 12447 isStatic: false 12448}; 12449let UniformGroup = _UniformGroup; 12450 12451class BindGroup { 12452 /** 12453 * Create a new instance eof the Bind Group. 12454 * @param resources - The resources that are bound together for use by a shader. 12455 */ 12456 constructor(resources) { 12457 /** The resources that are bound together for use by a shader. */ 12458 this.resources = /* @__PURE__ */ Object.create(null); 12459 this._dirty = true; 12460 let index = 0; 12461 for (const i in resources) { 12462 const resource = resources[i]; 12463 this.setResource(resource, index++); 12464 } 12465 this._updateKey(); 12466 } 12467 /** 12468 * Updates the key if its flagged as dirty. This is used internally to 12469 * match this bind group to a WebGPU BindGroup. 12470 * @internal 12471 */ 12472 _updateKey() { 12473 if (!this._dirty) 12474 return; 12475 this._dirty = false; 12476 const keyParts = []; 12477 let index = 0; 12478 for (const i in this.resources) { 12479 keyParts[index++] = this.resources[i]._resourceId; 12480 } 12481 this._key = keyParts.join("|"); 12482 } 12483 /** 12484 * Set a resource at a given index. this function will 12485 * ensure that listeners will be removed from the current resource 12486 * and added to the new resource. 12487 * @param resource - The resource to set. 12488 * @param index - The index to set the resource at. 12489 */ 12490 setResource(resource, index) { 12491 const currentResource = this.resources[index]; 12492 if (resource === currentResource) 12493 return; 12494 if (currentResource) { 12495 resource.off?.("change", this.onResourceChange, this); 12496 } 12497 resource.on?.("change", this.onResourceChange, this); 12498 this.resources[index] = resource; 12499 this._dirty = true; 12500 } 12501 /** 12502 * Returns the resource at the current specified index. 12503 * @param index - The index of the resource to get. 12504 * @returns - The resource at the specified index. 12505 */ 12506 getResource(index) { 12507 return this.resources[index]; 12508 } 12509 /** 12510 * Used internally to 'touch' each resource, to ensure that the GC 12511 * knows that all resources in this bind group are still being used. 12512 * @param now - The current time in milliseconds. 12513 * @param tick - The current tick. 12514 * @internal 12515 */ 12516 _touch(now, tick) { 12517 const resources = this.resources; 12518 for (const i in resources) { 12519 resources[i]._gcLastUsed = now; 12520 resources[i]._touched = tick; 12521 } 12522 } 12523 /** Destroys this bind group and removes all listeners. */ 12524 destroy() { 12525 const resources = this.resources; 12526 for (const i in resources) { 12527 const resource = resources[i]; 12528 resource?.off?.("change", this.onResourceChange, this); 12529 } 12530 this.resources = null; 12531 } 12532 onResourceChange(resource) { 12533 this._dirty = true; 12534 if (resource.destroyed) { 12535 const resources = this.resources; 12536 for (const i in resources) { 12537 if (resources[i] === resource) { 12538 resources[i] = null; 12539 } 12540 } 12541 } else { 12542 this._updateKey(); 12543 } 12544 } 12545} 12546 12547var RendererType = /* @__PURE__ */ ((RendererType2) => { 12548 RendererType2[RendererType2["WEBGL"] = 1] = "WEBGL"; 12549 RendererType2[RendererType2["WEBGPU"] = 2] = "WEBGPU"; 12550 RendererType2[RendererType2["BOTH"] = 3] = "BOTH"; 12551 return RendererType2; 12552})(RendererType || {}); 12553
12554class Shader extends EventEmitter { 12555 constructor(options) { 12556 super(); 12557 /** A unique identifier for the shader */ 12558 this.uid = uid("shader"); 12559 /** 12560 * A record of the uniform groups and resources used by the shader. 12561 * This is used by WebGL renderer to sync uniform data. 12562 * @internal 12563 */ 12564 this._uniformBindMap = /* @__PURE__ */ Object.create(null); 12565 this._ownedBindGroups = []; 12566 /** @internal */ 12567 this._destroyed = false; 12568 let { 12569 gpuProgram, 12570 glProgram, 12571 groups, 12572 resources, 12573 compatibleRenderers, 12574 groupMap 12575 } = options; 12576 this.gpuProgram = gpuProgram; 12577 this.glProgram = glProgram; 12578 if (compatibleRenderers === void 0) { 12579 compatibleRenderers = 0; 12580 if (gpuProgram) 12581 compatibleRenderers |= RendererType.WEBGPU; 12582 if (glProgram) 12583 compatibleRenderers |= RendererType.WEBGL; 12584 } 12585 this.compatibleRenderers = compatibleRenderers; 12586 const nameHash = {}; 12587 if (!resources && !groups) { 12588 resources = {}; 12589 } 12590 if (resources && groups) { 12591 throw new Error("[Shader] Cannot have both resources and groups"); 12592 } else if (!gpuProgram && groups && !groupMap) { 12593 throw new Error("[Shader] No group map or WebGPU shader provided - consider using resources instead."); 12594 } else if (!gpuProgram && groups && groupMap) { 12595 for (const i in groupMap) { 12596 for (const j in groupMap[i]) { 12597 const uniformName = groupMap[i][j]; 12598 nameHash[uniformName] = { 12599 group: i, 12600 binding: j, 12601 name: uniformName 12602 }; 12603 } 12604 } 12605 } else if (gpuProgram && groups && !groupMap) { 12606 const groupData = gpuProgram.structsAndGroups.groups; 12607 groupMap = {}; 12608 groupData.forEach((data) => { 12609 groupMap[data.group] = groupMap[data.group] || {}; 12610 groupMap[data.group][data.binding] = data.name; 12611 nameHash[data.name] = data; 12612 }); 12613 } else if (resources) { 12614 groups = {}; 12615 groupMap = {}; 12616 if (gpuProgram) { 12617 const groupData = gpuProgram.structsAndGroups.groups; 12618 groupData.forEach((data) => { 12619 groupMap[data.group] = groupMap[data.group] || {}; 12620 groupMap[data.group][data.binding] = data.name; 12621 nameHash[data.name] = data; 12622 }); 12623 } 12624 let bindTick = 0; 12625 for (const i in resources) { 12626 if (nameHash[i]) 12627 continue; 12628 if (!groups[99]) { 12629 groups[99] = new BindGroup(); 12630 this._ownedBindGroups.push(groups[99]); 12631 } 12632 nameHash[i] = { group: 99, binding: bindTick, name: i }; 12633 groupMap[99] = groupMap[99] || {}; 12634 groupMap[99][bindTick] = i; 12635 bindTick++; 12636 } 12637 for (const i in resources) { 12638 const name = i; 12639 let value = resources[i]; 12640 if (!value.source && !value._resourceType) { 12641 value = new UniformGroup(value); 12642 } 12643 const data = nameHash[name]; 12644 if (data) { 12645 if (!groups[data.group]) { 12646 groups[data.group] = new BindGroup(); 12647 this._ownedBindGroups.push(groups[data.group]); 12648 } 12649 groups[data.group].setResource(value, data.binding); 12650 } 12651 } 12652 } 12653 this.groups = groups; 12654 this._uniformBindMap = groupMap; 12655 this.resources = this._buildResourceAccessor(groups, nameHash); 12656 } 12657 /** 12658 * Sometimes a resource group will be provided later (for example global uniforms) 12659 * In such cases, this method can be used to let the shader know about the group. 12660 * @param name - the name of the resource group 12661 * @param groupIndex - the index of the group (should match the webGPU shader group location) 12662 * @param bindIndex - the index of the bind point (should match the webGPU shader bind point) 12663 */ 12664 addResource(name, groupIndex, bindIndex) { 12665 var _a, _b; 12666 (_a = this._uniformBindMap)[groupIndex] || (_a[groupIndex] = {}); 12667 (_b = this._uniformBindMap[groupIndex])[bindIndex] || (_b[bindIndex] = name); 12668 if (!this.groups[groupIndex]) { 12669 this.groups[groupIndex] = new BindGroup(); 12670 this._ownedBindGroups.push(this.groups[groupIndex]); 12671 } 12672 }
12673 _buildResourceAccessor(groups, nameHash) { 12674 const uniformsOut = {}; 12675 for (const i in nameHash) { 12676 const data = nameHash[i]; 12677 Object.defineProperty(uniformsOut, data.name, { 12678 get() { 12679 return groups[data.group].getResource(data.binding); 12680 }, 12681 set(value) { 12682 groups[data.group].setResource(value, data.binding); 12683 } 12684 }); 12685 } 12686 return uniformsOut; 12687 } 12688 /** 12689 * Use to destroy the shader when its not longer needed. 12690 * It will destroy the resources and remove listeners. 12691 * @param destroyPrograms - if the programs should be destroyed as well. 12692 * Make sure its not being used by other shaders! 12693 */ 12694 destroy(destroyPrograms = false) { 12695 if (this._destroyed) 12696 return; 12697 this._destroyed = true; 12698 this.emit("destroy", this); 12699 if (destroyPrograms) { 12700 this.gpuProgram?.destroy(); 12701 this.glProgram?.destroy(); 12702 } 12703 this.gpuProgram = null; 12704 this.glProgram = null; 12705 this.removeAllListeners(); 12706 this._uniformBindMap = null; 12707 this._ownedBindGroups.forEach((bindGroup) => { 12708 bindGroup.destroy(); 12709 }); 12710 this._ownedBindGroups = null; 12711 this.resources = null; 12712 this.groups = null; 12713 } 12714 static from(options) { 12715 const { gpu, gl, ...rest } = options; 12716 let gpuProgram; 12717 let glProgram; 12718 if (gpu) { 12719 gpuProgram = GpuProgram.from(gpu); 12720 } 12721 if (gl) { 12722 glProgram = GlProgram.from(gl); 12723 } 12724 return new Shader({ 12725 gpuProgram, 12726 glProgram, 12727 ...rest 12728 }); 12729 } 12730} 12731 12732const blendModeIds = { 12733 normal: 0, 12734 add: 1, 12735 multiply: 2, 12736 screen: 3, 12737 overlay: 4, 12738 erase: 5, 12739 "normal-npm": 6, 12740 "add-npm": 7, 12741 "screen-npm": 8, 12742 min: 9, 12743 max: 10 12744}; 12745const BLEND = 0; 12746const OFFSET = 1; 12747const CULLING = 2; 12748const DEPTH_TEST = 3; 12749const WINDING = 4; 12750const DEPTH_MASK = 5; 12751const _State = class _State { 12752 constructor() { 12753 this.data = 0; 12754 this.blendMode = "normal"; 12755 this.polygonOffset = 0; 12756 this.blend = true; 12757 this.depthMask = true; 12758 } 12759 /** 12760 * Activates blending of the computed fragment color values. 12761 * @default true 12762 */ 12763 get blend() { 12764 return !!(this.data & 1 << BLEND); 12765 } 12766 set blend(value) { 12767 if (!!(this.data & 1 << BLEND) !== value) { 12768 this.data ^= 1 << BLEND; 12769 } 12770 } 12771 /**
12772 * Activates adding an offset to depth values of polygon's fragments 12773 * @default false 12774 */ 12775 get offsets() { 12776 return !!(this.data & 1 << OFFSET); 12777 } 12778 set offsets(value) { 12779 if (!!(this.data & 1 << OFFSET) !== value) { 12780 this.data ^= 1 << OFFSET; 12781 } 12782 } 12783 /** The culling settings for this state none - No culling back - Back face culling front - Front face culling */ 12784 set cullMode(value) { 12785 if (value === "none") { 12786 this.culling = false; 12787 return; 12788 } 12789 this.culling = true; 12790 this.clockwiseFrontFace = value === "front"; 12791 } 12792 get cullMode() { 12793 if (!this.culling) { 12794 return "none"; 12795 } 12796 return this.clockwiseFrontFace ? "front" : "back"; 12797 } 12798 /** 12799 * Activates culling of polygons. 12800 * @default false 12801 */ 12802 get culling() { 12803 return !!(this.data & 1 << CULLING); 12804 } 12805 set culling(value) { 12806 if (!!(this.data & 1 << CULLING) !== value) { 12807 this.data ^= 1 << CULLING; 12808 } 12809 } 12810 /** 12811 * Activates depth comparisons and updates to the depth buffer. 12812 * @default false 12813 */ 12814 get depthTest() { 12815 return !!(this.data & 1 << DEPTH_TEST); 12816 } 12817 set depthTest(value) { 12818 if (!!(this.data & 1 << DEPTH_TEST) !== value) { 12819 this.data ^= 1 << DEPTH_TEST; 12820 } 12821 } 12822 /** 12823 * Enables or disables writing to the depth buffer. 12824 * @default true 12825 */ 12826 get depthMask() { 12827 return !!(this.data & 1 << DEPTH_MASK); 12828 } 12829 set depthMask(value) { 12830 if (!!(this.data & 1 << DEPTH_MASK) !== value) { 12831 this.data ^= 1 << DEPTH_MASK; 12832 } 12833 } 12834 /** 12835 * Specifies whether or not front or back-facing polygons can be culled. 12836 * @default false 12837 */ 12838 get clockwiseFrontFace() { 12839 return !!(this.data & 1 << WINDING); 12840 } 12841 set clockwiseFrontFace(value) { 12842 if (!!(this.data & 1 << WINDING) !== value) { 12843 this.data ^= 1 << WINDING; 12844 } 12845 } 12846 /** 12847 * The blend mode to be applied when this state is set. Apply a value of `normal` to reset the blend mode. 12848 * Setting this mode to anything other than NO_BLEND will automatically switch blending on. 12849 * @default 'normal' 12850 */ 12851 get blendMode() { 12852 return this._blendMode; 12853 } 12854 set blendMode(value) { 12855 this.blend = value !== "none"; 12856 this._blendMode = value; 12857 this._blendModeId = blendModeIds[value] || 0; 12858 } 12859 /** 12860 * The polygon offset. Setting this property to anything other than 0 will automatically enable polygon offset fill. 12861 * @default 0 12862 */ 12863 get polygonOffset() { 12864 return this._polygonOffset; 12865 } 12866 set polygonOffset(value) { 12867 this.offsets = !!value; 12868 this._polygonOffset = value; 12869 } 12870 toString() { 12871 return `[pixi.js/core:State blendMode=${this.blendMode} clockwiseFrontFace=${this.clockwiseFrontFace} culling=${this.culling} depthMask=${this.depthMask} polygonOffset=${this.polygonOffset}]`; 12872 } 12873 /** 12874 * A quickly getting an instance of a State that is configured for 2d rendering. 12875 * @returns a new State with values set for 2d rendering 12876 */ 12877 static for2d() { 12878 const state = new _State(); 12879 state.depthTest = false; 12880 state.blend = true; 12881 return state; 12882 } 12883}; 12884_State.default2d = _State.for2d(); 12885let State = _State; 12886 12887const _Filter = class _Filter extends Shader { 12888 /** 12889 * @param options - The optional parameters of this filter. 12890 */ 12891 constructor(options) { 12892 options = { ..._Filter.defaultOptions, ...options }; 12893 super(options); 12894 /** If enabled is true the filter is applied, if false it will not. */ 12895 this.enabled = true; 12896 /** 12897 * The gpu state the filter requires to render. 12898 * @internal 12899 */ 12900 this._state = State.for2d(); 12901 this.blendMode = options.blendMode; 12902 this.padding = options.padding; 12903 if (typeof options.antialias === "boolean") { 12904 this.antialias = options.antialias ? "on" : "off"; 12905 } else { 12906 this.antialias = options.antialias; 12907 } 12908 this.resolution = options.resolution; 12909 this.blendRequired = options.blendRequired; 12910 this.clipToViewport = options.clipToViewport; 12911 this.addResource("uTexture", 0, 1); 12912 if (options.blendRequired) { 12913 this.addResource("uBackTexture", 0, 3); 12914 } 12915 } 12916 /** 12917 * Applies the filter 12918 * @param filterManager - The renderer to retrieve the filter from 12919 * @param input - The input render target. 12920 * @param output - The target to output to. 12921 * @param clearMode - Should the output be cleared before rendering to it 12922 */ 12923 apply(filterManager, input, output, clearMode) { 12924 filterManager.applyFilter(this, input, output, clearMode); 12925 } 12926 /** 12927 * Get the blend mode of the filter. 12928 * @default "normal" 12929 */ 12930 get blendMode() { 12931 return this._state.blendMode; 12932 } 12933 /** Sets the blend mode of the filter. */ 12934 set blendMode(value) { 12935 this._state.blendMode = value; 12936 } 12937 /** 12938 * A short hand function to create a filter based of a vertex and fragment shader src. 12939 * @param options 12940 * @returns A shiny new PixiJS filter! 12941 */ 12942 static from(options) { 12943 const { gpu, gl, ...rest } = options; 12944 let gpuProgram; 12945 let glProgram; 12946 if (gpu) { 12947 gpuProgram = GpuProgram.from(gpu); 12948 } 12949 if (gl) { 12950 glProgram = GlProgram.from(gl); 12951 } 12952 return new _Filter({ 12953 gpuProgram, 12954 glProgram, 12955 ...rest 12956 }); 12957 } 12958}; 12959/** The default filter settings */ 12960_Filter.defaultOptions = { 12961 blendMode: "normal", 12962 resolution: 1, 12963 padding: 0, 12964 antialias: "off", 12965 blendRequired: false, 12966 clipToViewport: true 12967}; 12968let Filter = _Filter; 12969 12970const environments = []; 12971extensions.handleByNamedList(ExtensionType.Environment, environments); 12972async function loadEnvironmentExtensions(skip) { 12973 if (skip) 12974 return; 12975 for (let i = 0; i < environments.length; i++) { 12976 const env = environments[i]; 12977 if (env.value.test()) { 12978 await env.value.load(); 12979 return; 12980 } 12981 } 12982}
vendor: 5,111 bytes, lines 12983-13149
12983 12984let unsafeEval; 12985function unsafeEvalSupported() { 12986 if (typeof unsafeEval === "boolean") { 12987 return unsafeEval; 12988 } 12989 try { 12990 const func = new Function("param1", "param2", "param3", "return param1[param2] === param3;"); 12991 unsafeEval = func({ a: "b" }, "a", "b") === true; 12992 } catch (_e) { 12993 unsafeEval = false; 12994 } 12995 return unsafeEval; 12996} 12997 12998function earcut$1(data, holeIndices, dim = 2) { 12999 13000 const hasHoles = holeIndices && holeIndices.length; 13001 const outerLen = hasHoles ? holeIndices[0] * dim : data.length; 13002 let outerNode = linkedList(data, 0, outerLen, dim, true); 13003 const triangles = []; 13004 13005 if (!outerNode || outerNode.next === outerNode.prev) return triangles; 13006 13007 let minX, minY, invSize; 13008 13009 if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim); 13010 13011 // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox 13012 if (data.length > 80 * dim) { 13013 minX = data[0]; 13014 minY = data[1]; 13015 let maxX = minX; 13016 let maxY = minY; 13017 13018 for (let i = dim; i < outerLen; i += dim) { 13019 const x = data[i]; 13020 const y = data[i + 1]; 13021 if (x < minX) minX = x; 13022 if (y < minY) minY = y; 13023 if (x > maxX) maxX = x; 13024 if (y > maxY) maxY = y; 13025 } 13026 13027 // minX, minY and invSize are later used to transform coords into integers for z-order calculation 13028 invSize = Math.max(maxX - minX, maxY - minY); 13029 invSize = invSize !== 0 ? 32767 / invSize : 0; 13030 } 13031 13032 earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0); 13033 13034 return triangles; 13035} 13036 13037// create a circular doubly linked list from polygon points in the specified winding order 13038function linkedList(data, start, end, dim, clockwise) { 13039 let last; 13040 13041 if (clockwise === (signedArea(data, start, end, dim) > 0)) { 13042 for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last); 13043 } else { 13044 for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last); 13045 } 13046 13047 if (last && equals(last, last.next)) { 13048 removeNode(last); 13049 last = last.next; 13050 } 13051 13052 return last; 13053} 13054 13055// eliminate colinear or duplicate points 13056function filterPoints(start, end) { 13057 if (!start) return start; 13058 if (!end) end = start; 13059 13060 let p = start, 13061 again; 13062 do { 13063 again = false; 13064 13065 if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) { 13066 removeNode(p); 13067 p = end = p.prev; 13068 if (p === p.next) break; 13069 again = true; 13070 13071 } else { 13072 p = p.next; 13073 } 13074 } while (again || p !== end); 13075 13076 return end; 13077} 13078 13079// main ear slicing loop which triangulates a polygon (given as a linked list) 13080function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) { 13081 if (!ear) return; 13082 13083 // interlink polygon nodes in z-order 13084 if (!pass && invSize) indexCurve(ear, minX, minY, invSize); 13085 13086 let stop = ear; 13087 13088 // iterate through ears, slicing them one by one 13089 while (ear.prev !== ear.next) { 13090 const prev = ear.prev; 13091 const next = ear.next; 13092 13093 if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) { 13094 triangles.push(prev.i, ear.i, next.i); // cut off the triangle 13095 13096 removeNode(ear); 13097 13098 // skipping the next vertex leads to less sliver triangles 13099 ear = next.next; 13100 stop = next.next; 13101 13102 continue; 13103 } 13104 13105 ear = next; 13106 13107 // if we looped through the whole remaining polygon and can't find any more ears 13108 if (ear === stop) { 13109 // try filtering points and slicing again 13110 if (!pass) { 13111 earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1); 13112 13113 // if this didn't work, try curing all small self-intersections locally 13114 } else if (pass === 1) { 13115 ear = cureLocalIntersections(filterPoints(ear), triangles); 13116 earcutLinked(ear, triangles, dim, minX, minY, invSize, 2); 13117 13118 // as a last resort, try splitting the remaining polygon into two 13119 } else if (pass === 2) { 13120 splitEarcut(ear, triangles, dim, minX, minY, invSize); 13121 } 13122 13123 break; 13124 } 13125 } 13126} 13127 13128// check whether a polygon node forms a valid ear with adjacent nodes 13129function isEar(ear) { 13130 const a = ear.prev, 13131 b = ear, 13132 c = ear.next; 13133 13134 if (area(a, b, c) >= 0) return false; // reflex, can't be an ear 13135 13136 // now make sure we don't have other points inside the potential ear 13137 const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y; 13138 13139 // triangle bbox 13140 const x0 = Math.min(ax, bx, cx), 13141 y0 = Math.min(ay, by, cy), 13142 x1 = Math.max(ax, bx, cx), 13143 y1 = Math.max(ay, by, cy); 13144 13145 let p = c.next; 13146 while (p !== a) { 13147 if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && 13148 pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && 13149 area(p.prev, p, p.next) >= 0) return false;
vendor: 14,700 bytes, lines 13150-13629
13150 p = p.next; 13151 } 13152 13153 return true; 13154} 13155 13156function isEarHashed(ear, minX, minY, invSize) { 13157 const a = ear.prev, 13158 b = ear, 13159 c = ear.next; 13160 13161 if (area(a, b, c) >= 0) return false; // reflex, can't be an ear 13162 13163 const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y; 13164 13165 // triangle bbox 13166 const x0 = Math.min(ax, bx, cx), 13167 y0 = Math.min(ay, by, cy), 13168 x1 = Math.max(ax, bx, cx), 13169 y1 = Math.max(ay, by, cy); 13170 13171 // z-order range for the current triangle bbox; 13172 const minZ = zOrder(x0, y0, minX, minY, invSize), 13173 maxZ = zOrder(x1, y1, minX, minY, invSize); 13174 13175 let p = ear.prevZ, 13176 n = ear.nextZ; 13177 13178 // look for points inside the triangle in both directions 13179 while (p && p.z >= minZ && n && n.z <= maxZ) { 13180 if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c && 13181 pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false; 13182 p = p.prevZ; 13183 13184 if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c && 13185 pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false; 13186 n = n.nextZ; 13187 } 13188 13189 // look for remaining points in decreasing z-order 13190 while (p && p.z >= minZ) { 13191 if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c && 13192 pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false; 13193 p = p.prevZ; 13194 } 13195 13196 // look for remaining points in increasing z-order 13197 while (n && n.z <= maxZ) { 13198 if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c && 13199 pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false; 13200 n = n.nextZ; 13201 } 13202 13203 return true; 13204} 13205 13206// go through all polygon nodes and cure small local self-intersections 13207function cureLocalIntersections(start, triangles) { 13208 let p = start; 13209 do { 13210 const a = p.prev, 13211 b = p.next.next; 13212 13213 if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) { 13214 13215 triangles.push(a.i, p.i, b.i); 13216 13217 // remove two nodes involved 13218 removeNode(p); 13219 removeNode(p.next); 13220 13221 p = start = b; 13222 } 13223 p = p.next; 13224 } while (p !== start); 13225 13226 return filterPoints(p); 13227} 13228 13229// try splitting polygon into two and triangulate them independently 13230function splitEarcut(start, triangles, dim, minX, minY, invSize) { 13231 // look for a valid diagonal that divides the polygon into two 13232 let a = start; 13233 do { 13234 let b = a.next.next; 13235 while (b !== a.prev) { 13236 if (a.i !== b.i && isValidDiagonal(a, b)) { 13237 // split the polygon in two by the diagonal 13238 let c = splitPolygon(a, b); 13239 13240 // filter colinear points around the cuts 13241 a = filterPoints(a, a.next); 13242 c = filterPoints(c, c.next); 13243 13244 // run earcut on each half 13245 earcutLinked(a, triangles, dim, minX, minY, invSize, 0); 13246 earcutLinked(c, triangles, dim, minX, minY, invSize, 0); 13247 return; 13248 } 13249 b = b.next; 13250 } 13251 a = a.next; 13252 } while (a !== start); 13253} 13254 13255// link every hole into the outer loop, producing a single-ring polygon without holes 13256function eliminateHoles(data, holeIndices, outerNode, dim) { 13257 const queue = []; 13258 13259 for (let i = 0, len = holeIndices.length; i < len; i++) { 13260 const start = holeIndices[i] * dim; 13261 const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; 13262 const list = linkedList(data, start, end, dim, false); 13263 if (list === list.next) list.steiner = true; 13264 queue.push(getLeftmost(list)); 13265 } 13266 13267 queue.sort(compareXYSlope); 13268 13269 // process holes from left to right 13270 for (let i = 0; i < queue.length; i++) { 13271 outerNode = eliminateHole(queue[i], outerNode); 13272 } 13273 13274 return outerNode; 13275} 13276 13277function compareXYSlope(a, b) { 13278 let result = a.x - b.x; 13279 // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find 13280 // the bridge to the outer shell is always the point that they meet at. 13281 if (result === 0) { 13282 result = a.y - b.y; 13283 if (result === 0) { 13284 const aSlope = (a.next.y - a.y) / (a.next.x - a.x); 13285 const bSlope = (b.next.y - b.y) / (b.next.x - b.x); 13286 result = aSlope - bSlope; 13287 } 13288 } 13289 return result; 13290} 13291 13292// find a bridge between vertices that connects hole with an outer ring and link it 13293function eliminateHole(hole, outerNode) { 13294 const bridge = findHoleBridge(hole, outerNode); 13295 if (!bridge) { 13296 return outerNode; 13297 } 13298 13299 const bridgeReverse = splitPolygon(bridge, hole); 13300 13301 // filter collinear points around the cuts 13302 filterPoints(bridgeReverse, bridgeReverse.next); 13303 return filterPoints(bridge, bridge.next); 13304} 13305 13306// David Eberly's algorithm for finding a bridge between hole and outer polygon 13307function findHoleBridge(hole, outerNode) { 13308 let p = outerNode; 13309 const hx = hole.x; 13310 const hy = hole.y; 13311 let qx = -Infinity; 13312 let m; 13313 13314 // find a segment intersected by a ray from the hole's leftmost point to the left; 13315 // segment's endpoint with lesser x will be potential connection point 13316 // unless they intersect at a vertex, then choose the vertex 13317 if (equals(hole, p)) return p; 13318 do { 13319 if (equals(hole, p.next)) return p.next; 13320 else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) { 13321 const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y); 13322 if (x <= hx && x > qx) { 13323 qx = x; 13324 m = p.x < p.next.x ? p : p.next; 13325 if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint 13326 } 13327 } 13328 p = p.next; 13329 } while (p !== outerNode); 13330 13331 if (!m) return null; 13332 13333 // look for points inside the triangle of hole point, segment intersection and endpoint; 13334 // if there are no points found, we have a valid connection; 13335 // otherwise choose the point of the minimum angle with the ray as connection point 13336 13337 const stop = m; 13338 const mx = m.x; 13339 const my = m.y; 13340 let tanMin = Infinity; 13341 13342 p = m; 13343 13344 do { 13345 if (hx >= p.x && p.x >= mx && hx !== p.x && 13346 pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) { 13347 13348 const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential 13349 13350 if (locallyInside(p, hole) && 13351 (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) { 13352 m = p; 13353 tanMin = tan; 13354 } 13355 } 13356 13357 p = p.next; 13358 } while (p !== stop); 13359 13360 return m; 13361} 13362 13363// whether sector in vertex m contains sector in vertex p in the same coordinates 13364function sectorContainsSector(m, p) { 13365 return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0; 13366} 13367 13368// interlink polygon nodes in z-order 13369function indexCurve(start, minX, minY, invSize) { 13370 let p = start; 13371 do { 13372 if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize); 13373 p.prevZ = p.prev; 13374 p.nextZ = p.next; 13375 p = p.next; 13376 } while (p !== start); 13377 13378 p.prevZ.nextZ = null; 13379 p.prevZ = null; 13380 13381 sortLinked(p); 13382} 13383 13384// Simon Tatham's linked list merge sort algorithm 13385// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html 13386function sortLinked(list) { 13387 let numMerges; 13388 let inSize = 1; 13389 13390 do { 13391 let p = list; 13392 let e; 13393 list = null; 13394 let tail = null; 13395 numMerges = 0; 13396 13397 while (p) { 13398 numMerges++; 13399 let q = p; 13400 let pSize = 0; 13401 for (let i = 0; i < inSize; i++) { 13402 pSize++; 13403 q = q.nextZ; 13404 if (!q) break; 13405 } 13406 let qSize = inSize; 13407 13408 while (pSize > 0 || (qSize > 0 && q)) { 13409 13410 if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) { 13411 e = p; 13412 p = p.nextZ; 13413 pSize--; 13414 } else { 13415 e = q; 13416 q = q.nextZ; 13417 qSize--; 13418 } 13419 13420 if (tail) tail.nextZ = e; 13421 else list = e; 13422 13423 e.prevZ = tail; 13424 tail = e; 13425 } 13426 13427 p = q; 13428 } 13429 13430 tail.nextZ = null; 13431 inSize *= 2; 13432 13433 } while (numMerges > 1); 13434 13435 return list; 13436} 13437 13438// z-order of a point given coords and inverse of the longer side of data bbox 13439function zOrder(x, y, minX, minY, invSize) { 13440 // coords are transformed into non-negative 15-bit integer range 13441 x = (x - minX) * invSize | 0; 13442 y = (y - minY) * invSize | 0; 13443 13444 x = (x | (x << 8)) & 0x00FF00FF; 13445 x = (x | (x << 4)) & 0x0F0F0F0F; 13446 x = (x | (x << 2)) & 0x33333333; 13447 x = (x | (x << 1)) & 0x55555555; 13448 13449 y = (y | (y << 8)) & 0x00FF00FF; 13450 y = (y | (y << 4)) & 0x0F0F0F0F; 13451 y = (y | (y << 2)) & 0x33333333; 13452 y = (y | (y << 1)) & 0x55555555; 13453 13454 return x | (y << 1); 13455} 13456 13457// find the leftmost node of a polygon ring 13458function getLeftmost(start) { 13459 let p = start, 13460 leftmost = start; 13461 do { 13462 if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p; 13463 p = p.next; 13464 } while (p !== start); 13465 13466 return leftmost; 13467} 13468 13469// check if a point lies within a convex triangle 13470function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) { 13471 return (cx - px) * (ay - py) >= (ax - px) * (cy - py) && 13472 (ax - px) * (by - py) >= (bx - px) * (ay - py) && 13473 (bx - px) * (cy - py) >= (cx - px) * (by - py); 13474} 13475 13476// check if a point lies within a convex triangle but false if its equal to the first point of the triangle 13477function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) { 13478 return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py); 13479} 13480 13481// check if a diagonal between two polygon nodes is valid (lies in polygon interior) 13482function isValidDiagonal(a, b) { 13483 return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges 13484 (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible 13485 (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors 13486 equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case 13487} 13488 13489// signed area of a triangle 13490function area(p, q, r) { 13491 return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); 13492} 13493 13494// check if two points are equal 13495function equals(p1, p2) { 13496 return p1.x === p2.x && p1.y === p2.y; 13497} 13498 13499// check if two segments intersect 13500function intersects(p1, q1, p2, q2) { 13501 const o1 = sign(area(p1, q1, p2)); 13502 const o2 = sign(area(p1, q1, q2)); 13503 const o3 = sign(area(p2, q2, p1)); 13504 const o4 = sign(area(p2, q2, q1)); 13505 13506 if (o1 !== o2 && o3 !== o4) return true; // general case 13507 13508 if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1 13509 if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1 13510 if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2 13511 if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2 13512 13513 return false; 13514} 13515 13516// for collinear points p, q, r, check if point q lies on segment pr 13517function onSegment(p, q, r) { 13518 return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y); 13519} 13520 13521function sign(num) { 13522 return num > 0 ? 1 : num < 0 ? -1 : 0; 13523} 13524 13525// check if a polygon diagonal intersects any polygon segments 13526function intersectsPolygon(a, b) { 13527 let p = a; 13528 do { 13529 if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i && 13530 intersects(p, p.next, a, b)) return true; 13531 p = p.next; 13532 } while (p !== a); 13533 13534 return false; 13535} 13536 13537// check if a polygon diagonal is locally inside the polygon 13538function locallyInside(a, b) { 13539 return area(a.prev, a, a.next) < 0 ? 13540 area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 : 13541 area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; 13542} 13543 13544// check if the middle point of a polygon diagonal is inside the polygon 13545function middleInside(a, b) { 13546 let p = a; 13547 let inside = false; 13548 const px = (a.x + b.x) / 2; 13549 const py = (a.y + b.y) / 2; 13550 do { 13551 if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y && 13552 (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)) 13553 inside = !inside; 13554 p = p.next; 13555 } while (p !== a); 13556 13557 return inside; 13558} 13559 13560// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two; 13561// if one belongs to the outer ring and another to a hole, it merges it into a single ring 13562function splitPolygon(a, b) { 13563 const a2 = createNode(a.i, a.x, a.y), 13564 b2 = createNode(b.i, b.x, b.y), 13565 an = a.next, 13566 bp = b.prev; 13567 13568 a.next = b; 13569 b.prev = a; 13570 13571 a2.next = an; 13572 an.prev = a2; 13573 13574 b2.next = a2; 13575 a2.prev = b2; 13576 13577 bp.next = b2; 13578 b2.prev = bp; 13579 13580 return b2; 13581} 13582 13583// create a node and optionally link it with previous one (in a circular doubly linked list) 13584function insertNode(i, x, y, last) { 13585 const p = createNode(i, x, y); 13586 13587 if (!last) { 13588 p.prev = p; 13589 p.next = p; 13590 13591 } else { 13592 p.next = last.next; 13593 p.prev = last; 13594 last.next.prev = p; 13595 last.next = p; 13596 } 13597 return p; 13598} 13599 13600function removeNode(p) { 13601 p.next.prev = p.prev; 13602 p.prev.next = p.next; 13603 13604 if (p.prevZ) p.prevZ.nextZ = p.nextZ; 13605 if (p.nextZ) p.nextZ.prevZ = p.prevZ; 13606} 13607 13608function createNode(i, x, y) { 13609 return { 13610 i, // vertex index in coordinates array 13611 x, y, // vertex coordinates 13612 prev: null, // previous and next vertex nodes in a polygon ring 13613 next: null, 13614 z: 0, // z-order curve value 13615 prevZ: null, // previous and next nodes in z-order 13616 nextZ: null, 13617 steiner: false // indicates whether this is a steiner point 13618 }; 13619} 13620 13621function signedArea(data, start, end, dim) { 13622 let sum = 0; 13623 for (let i = start, j = end - dim; i < end; i += dim) { 13624 sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); 13625 j = i; 13626 } 13627 return sum; 13628} 13629
13630const earcut = earcut$1.default || earcut$1; 13631 13632var CLEAR = /* @__PURE__ */ ((CLEAR2) => { 13633 CLEAR2[CLEAR2["NONE"] = 0] = "NONE"; 13634 CLEAR2[CLEAR2["COLOR"] = 16384] = "COLOR"; 13635 CLEAR2[CLEAR2["STENCIL"] = 1024] = "STENCIL"; 13636 CLEAR2[CLEAR2["DEPTH"] = 256] = "DEPTH"; 13637 CLEAR2[CLEAR2["COLOR_DEPTH"] = 16640] = "COLOR_DEPTH"; 13638 CLEAR2[CLEAR2["COLOR_STENCIL"] = 17408] = "COLOR_STENCIL"; 13639 CLEAR2[CLEAR2["DEPTH_STENCIL"] = 1280] = "DEPTH_STENCIL"; 13640 CLEAR2[CLEAR2["ALL"] = 17664] = "ALL"; 13641 return CLEAR2; 13642})(CLEAR || {}); 13643 13644class SystemRunner { 13645 /** 13646 * @param name - The function name that will be executed on the listeners added to this Runner. 13647 */ 13648 constructor(name) { 13649 this.items = []; 13650 this._name = name; 13651 } 13652 /* jsdoc/check-param-names */ 13653 /** 13654 * Dispatch/Broadcast Runner to all listeners added to the queue. 13655 * @param {...any} params - (optional) parameters to pass to each listener 13656 */ 13657 /* jsdoc/check-param-names */ 13658 emit(a0, a1, a2, a3, a4, a5, a6, a7) { 13659 const { name, items } = this; 13660 for (let i = 0, len = items.length; i < len; i++) { 13661 items[i][name](a0, a1, a2, a3, a4, a5, a6, a7); 13662 } 13663 return this; 13664 } 13665 /** 13666 * Add a listener to the Runner 13667 * 13668 * Runners do not need to have scope or functions passed to them. 13669 * All that is required is to pass the listening object and ensure that it has contains a function that has the same name 13670 * as the name provided to the Runner when it was created. 13671 * 13672 * Eg A listener passed to this Runner will require a 'complete' function. 13673 * 13674 * ```ts 13675 * import { Runner } from 'pixi.js'; 13676 * 13677 * const complete = new Runner('complete'); 13678 * ``` 13679 * 13680 * The scope used will be the object itself. 13681 * @param {any} item - The object that will be listening. 13682 */ 13683 add(item) { 13684 if (item[this._name]) { 13685 this.remove(item); 13686 this.items.push(item); 13687 } 13688 return this; 13689 } 13690 /** 13691 * Remove a single listener from the dispatch queue. 13692 * @param {any} item - The listener that you would like to remove. 13693 */ 13694 remove(item) { 13695 const index = this.items.indexOf(item); 13696 if (index !== -1) { 13697 this.items.splice(index, 1); 13698 } 13699 return this; 13700 } 13701 /** 13702 * Check to see if the listener is already in the Runner 13703 * @param {any} item - The listener that you would like to check. 13704 */ 13705 contains(item) { 13706 return this.items.indexOf(item) !== -1; 13707 } 13708 /** Remove all listeners from the Runner */ 13709 removeAll() { 13710 this.items.length = 0; 13711 return this; 13712 } 13713 /** Remove all references, don't use after this. */ 13714 destroy() { 13715 this.removeAll(); 13716 this.items = null; 13717 this._name = null; 13718 } 13719 /**
13720 * `true` if there are no this Runner contains no listeners 13721 * @readonly 13722 */ 13723 get empty() { 13724 return this.items.length === 0; 13725 } 13726 /** 13727 * The name of the runner. 13728 * @readonly 13729 */ 13730 get name() { 13731 return this._name; 13732 } 13733} 13734 13735const defaultRunners = [ 13736 "init", 13737 "destroy", 13738 "contextChange", 13739 "resolutionChange", 13740 "resetState", 13741 "renderEnd", 13742 "renderStart", 13743 "render", 13744 "update", 13745 "postrender", 13746 "prerender" 13747]; 13748const _AbstractRenderer = class _AbstractRenderer extends EventEmitter { 13749 /** 13750 * Set up a system with a collection of SystemClasses and runners. 13751 * Systems are attached dynamically to this class when added. 13752 * @param config - the config for the system manager 13753 */ 13754 constructor(config) { 13755 super(); 13756 /** The current tick of the renderer. */ 13757 this.tick = 0; 13758 /** @internal */ 13759 this.uid = uid("renderer"); 13760 /** @internal */ 13761 this.runners = /* @__PURE__ */ Object.create(null); 13762 /** @internal */ 13763 this.renderPipes = /* @__PURE__ */ Object.create(null); 13764 this._initOptions = {}; 13765 this._systemsHash = /* @__PURE__ */ Object.create(null); 13766 this.type = config.type; 13767 this.name = config.name; 13768 this.config = config; 13769 const combinedRunners = [...defaultRunners, ...this.config.runners ?? []]; 13770 this._addRunners(...combinedRunners); 13771 this._unsafeEvalCheck(); 13772 } 13773 /** 13774 * Initialize the renderer. 13775 * @param options - The options to use to create the renderer. 13776 */ 13777 async init(options = {}) { 13778 const skip = options.skipExtensionImports === true ? true : options.manageImports === false; 13779 await loadEnvironmentExtensions(skip); 13780 this._addSystems(this.config.systems); 13781 this._addPipes(this.config.renderPipes, this.config.renderPipeAdaptors); 13782 for (const systemName in this._systemsHash) { 13783 const system = this._systemsHash[systemName]; 13784 const defaultSystemOptions = system.constructor.defaultOptions; 13785 options = { ...defaultSystemOptions, ...options }; 13786 } 13787 options = { ..._AbstractRenderer.defaultOptions, ...options }; 13788 this._roundPixels = options.roundPixels ? 1 : 0; 13789 for (let i = 0; i < this.runners.init.items.length; i++) { 13790 await this.runners.init.items[i].init(options); 13791 } 13792 this._initOptions = options; 13793 } 13794 render(args, deprecated) { 13795 this.tick++; 13796 let options = args; 13797 if (options instanceof Container) { 13798 options = { container: options }; 13799 if (deprecated) { 13800 deprecation(v8_0_0, "passing a second argument is deprecated, please use render options instead"); 13801 options.target = deprecated.renderTexture; 13802 } 13803 } 13804 options.target || (options.target = this.view.renderTarget); 13805 if (options.target === this.view.renderTarget) { 13806 this._lastObjectRendered = options.container; 13807 options.clearColor ?? (options.clearColor = this.background.colorRgba); 13808 options.clear ?? (options.clear = this.background.clearBeforeRender); 13809 } 13810 if (options.clearColor) { 13811 const isRGBAArray = Array.isArray(options.clearColor) && options.clearColor.length === 4; 13812 options.clearColor = isRGBAArray ? options.clearColor : Color.shared.setValue(options.clearColor).toArray(); 13813 } 13814 if (!options.transform) { 13815 options.container.updateLocalTransform(); 13816 options.transform = options.container.localTransform; 13817 } 13818 if (!options.container.visible) { 13819 return; 13820 } 13821 options.container.enableRenderGroup(); 13822 this.runners.prerender.emit(options); 13823 this.runners.renderStart.emit(options); 13824 this.runners.render.emit(options); 13825 this.runners.renderEnd.emit(options); 13826 this.runners.postrender.emit(options); 13827 } 13828 /** 13829 * Resizes the WebGL view to the specified width and height. 13830 * @param desiredScreenWidth - The desired width of the screen. 13831 * @param desiredScreenHeight - The desired height of the screen. 13832 * @param resolution - The resolution / device pixel ratio of the renderer. 13833 */ 13834 resize(desiredScreenWidth, desiredScreenHeight, resolution) { 13835 const previousResolution = this.view.resolution; 13836 this.view.resize(desiredScreenWidth, desiredScreenHeight, resolution);
13837 this.emit("resize", this.view.screen.width, this.view.screen.height, this.view.resolution); 13838 if (resolution !== void 0 && resolution !== previousResolution) { 13839 this.runners.resolutionChange.emit(resolution); 13840 } 13841 } 13842 /** 13843 * Clears the render target. 13844 * @param options - The options to use when clearing the render target. 13845 * @param options.target - The render target to clear. 13846 * @param options.clearColor - The color to clear with. 13847 * @param options.clear - The clear mode to use. 13848 * @advanced 13849 */ 13850 clear(options = {}) { 13851 const renderer = this; 13852 options.target || (options.target = renderer.renderTarget.renderTarget); 13853 options.clearColor || (options.clearColor = this.background.colorRgba); 13854 options.clear ?? (options.clear = CLEAR.ALL); 13855 const { clear, clearColor, target } = options; 13856 Color.shared.setValue(clearColor ?? this.background.colorRgba); 13857 renderer.renderTarget.clear(target, clear, Color.shared.toArray()); 13858 } 13859 /** The resolution / device pixel ratio of the renderer. */ 13860 get resolution() { 13861 return this.view.resolution; 13862 } 13863 set resolution(value) { 13864 this.view.resolution = value; 13865 this.runners.resolutionChange.emit(value); 13866 } 13867 /** 13868 * Same as view.width, actual number of pixels in the canvas by horizontal. 13869 * @type {number} 13870 * @readonly 13871 * @default 800 13872 */ 13873 get width() { 13874 return this.view.texture.frame.width; 13875 } 13876 /** 13877 * Same as view.height, actual number of pixels in the canvas by vertical. 13878 * @default 600 13879 */ 13880 get height() { 13881 return this.view.texture.frame.height; 13882 } 13883 // NOTE: this was `view` in v7 13884 /** 13885 * The canvas element that everything is drawn to. 13886 * @type {environment.ICanvas} 13887 */ 13888 get canvas() { 13889 return this.view.canvas; 13890 } 13891 /** 13892 * the last object rendered by the renderer. Useful for other plugins like interaction managers 13893 * @readonly 13894 */ 13895 get lastObjectRendered() { 13896 return this._lastObjectRendered; 13897 } 13898 /** 13899 * Flag if we are rendering to the screen vs renderTexture 13900 * @readonly 13901 * @default true 13902 */ 13903 get renderingToScreen() { 13904 const renderer = this; 13905 return renderer.renderTarget.renderingToScreen; 13906 } 13907 /** 13908 * Measurements of the screen. (0, 0, screenWidth, screenHeight). 13909 * 13910 * Its safe to use as filterArea or hitArea for the whole stage. 13911 */ 13912 get screen() { 13913 return this.view.screen; 13914 } 13915 /** 13916 * Create a bunch of runners based of a collection of ids 13917 * @param runnerIds - the runner ids to add 13918 */ 13919 _addRunners(...runnerIds) { 13920 runnerIds.forEach((runnerId) => { 13921 this.runners[runnerId] = new SystemRunner(runnerId); 13922 }); 13923 } 13924 _addSystems(systems) { 13925 let i; 13926 for (i in systems) { 13927 const val = systems[i]; 13928 this._addSystem(val.value, val.name); 13929 } 13930 } 13931 /** 13932 * Add a new system to the renderer. 13933 * @param ClassRef - Class reference 13934 * @param name - Property name for system, if not specified 13935 * will use a static `name` property on the class itself. This 13936 * name will be assigned as s property on the Renderer so make 13937 * sure it doesn't collide with properties on Renderer. 13938 * @returns Return instance of renderer 13939 */ 13940 _addSystem(ClassRef, name) { 13941 const system = new ClassRef(this); 13942 if (this[name]) { 13943 throw new Error(`Whoops! The name "${name}" is already in use`); 13944 } 13945 this[name] = system; 13946 this._systemsHash[name] = system; 13947 for (const i in this.runners) { 13948 this.runners[i].add(system); 13949 } 13950 return this; 13951 } 13952 _addPipes(pipes, pipeAdaptors) { 13953 const adaptors = pipeAdaptors.reduce((acc, adaptor) => { 13954 acc[adaptor.name] = adaptor.value; 13955 return acc; 13956 }, {}); 13957 pipes.forEach((pipe) => { 13958 const PipeClass = pipe.value; 13959 const name = pipe.name; 13960 const Adaptor = adaptors[name]; 13961 this.renderPipes[name] = new PipeClass( 13962 this, 13963 Adaptor ? new Adaptor() : null 13964 ); 13965 this.runners.destroy.add(this.renderPipes[name]); 13966 }); 13967 } 13968 destroy(options = false) { 13969 this.runners.destroy.items.reverse(); 13970 this.runners.destroy.emit(options); 13971 if (options === true || typeof options === "object" && options.releaseGlobalResources) { 13972 GlobalResourceRegistry.release(); 13973 }
13974 Object.values(this.runners).forEach((runner) => { 13975 runner.destroy(); 13976 }); 13977 this._systemsHash = null; 13978 this.renderPipes = null; 13979 } 13980 /** 13981 * Generate a texture from a container. 13982 * @param options - options or container target to use when generating the texture 13983 * @returns a texture 13984 */ 13985 generateTexture(options) { 13986 return this.textureGenerator.generateTexture(options); 13987 } 13988 /** 13989 * Whether the renderer will round coordinates to whole pixels when rendering. 13990 * Can be overridden on a per scene item basis. 13991 */ 13992 get roundPixels() { 13993 return !!this._roundPixels; 13994 } 13995 /** 13996 * Overridable function by `pixi.js/unsafe-eval` to silence 13997 * throwing an error if platform doesn't support unsafe-evals. 13998 * @private 13999 * @ignore 14000 */ 14001 _unsafeEvalCheck() { 14002 if (!unsafeEvalSupported()) { 14003 throw new Error("Current environment does not allow unsafe-eval, please use pixi.js/unsafe-eval module to enable support."); 14004 } 14005 } 14006 /** 14007 * Resets the rendering state of the renderer. 14008 * This is useful when you want to use the WebGL context directly and need to ensure PixiJS's internal state 14009 * stays synchronized. When modifying the WebGL context state externally, calling this method before the next Pixi 14010 * render will reset all internal caches and ensure it executes correctly. 14011 * 14012 * This is particularly useful when combining PixiJS with other rendering engines like Three.js: 14013 * ```js 14014 * // Reset Three.js state 14015 * threeRenderer.resetState(); 14016 * 14017 * // Render a Three.js scene 14018 * threeRenderer.render(threeScene, threeCamera); 14019 * 14020 * // Reset PixiJS state since Three.js modified the WebGL context 14021 * pixiRenderer.resetState(); 14022 * 14023 * // Now render Pixi content 14024 * pixiRenderer.render(pixiScene); 14025 * ``` 14026 * @advanced 14027 */ 14028 resetState() { 14029 this.runners.resetState.emit(); 14030 } 14031}; 14032/** The default options for the renderer. */ 14033_AbstractRenderer.defaultOptions = { 14034 /** 14035 * Default resolution / device pixel ratio of the renderer. 14036 * @default 1 14037 */ 14038 resolution: 1, 14039 /** 14040 * Should the `failIfMajorPerformanceCaveat` flag be enabled as a context option used in the `isWebGLSupported` 14041 * function. If set to true, a WebGL renderer can fail to be created if the browser thinks there could be 14042 * performance issues when using WebGL. 14043 * 14044 * In PixiJS v6 this has changed from true to false by default, to allow WebGL to work in as many 14045 * scenarios as possible. However, some users may have a poor experience, for example, if a user has a gpu or 14046 * driver version blacklisted by the 14047 * browser. 14048 * 14049 * If your application requires high performance rendering, you may wish to set this to false. 14050 * We recommend one of two options if you decide to set this flag to false: 14051 * 14052 * 1: Use the Canvas renderer as a fallback in case high performance WebGL is 14053 * not supported. 14054 * 14055 * 2: Call `isWebGLSupported` (which if found in the utils package) in your code before attempting to create a 14056 * PixiJS renderer, and show an error message to the user if the function returns false, explaining that their 14057 * device & browser combination does not support high performance WebGL. 14058 * This is a much better strategy than trying to create a PixiJS renderer and finding it then fails. 14059 * @default false 14060 */ 14061 failIfMajorPerformanceCaveat: false, 14062 /** 14063 * Should round pixels be forced when rendering? 14064 * @default false 14065 */ 14066 roundPixels: false 14067}; 14068let AbstractRenderer = _AbstractRenderer; 14069 14070let _isWebGLSupported; 14071function isWebGLSupported(failIfMajorPerformanceCaveat) { 14072 if (_isWebGLSupported !== void 0) 14073 return _isWebGLSupported; 14074 _isWebGLSupported = (() => { 14075 const contextOptions = { 14076 stencil: true, 14077 failIfMajorPerformanceCaveat: failIfMajorPerformanceCaveat ?? AbstractRenderer.defaultOptions.failIfMajorPerformanceCaveat 14078 }; 14079 try { 14080 if (!DOMAdapter.get().getWebGLRenderingContext()) { 14081 return false; 14082 } 14083 const canvas = DOMAdapter.get().createCanvas(); 14084 let gl = canvas.getContext("webgl", contextOptions); 14085 const success = !!gl?.getContextAttributes()?.stencil; 14086 if (gl) { 14087 const loseContext = gl.getExtension("WEBGL_lose_context"); 14088 if (loseContext) { 14089 loseContext.loseContext(); 14090 } 14091 } 14092 gl = null; 14093 return success; 14094 } catch (_e) { 14095 return false;
14096 } 14097 })(); 14098 return _isWebGLSupported; 14099} 14100 14101let _isWebGPUSupported; 14102async function isWebGPUSupported(options = {}) { 14103 if (_isWebGPUSupported !== void 0) 14104 return _isWebGPUSupported; 14105 _isWebGPUSupported = await (async () => { 14106 const gpu = DOMAdapter.get().getNavigator().gpu; 14107 if (!gpu) { 14108 return false; 14109 } 14110 try { 14111 const adapter = await gpu.requestAdapter(options); 14112 await adapter.requestDevice(); 14113 return true; 14114 } catch (_e) { 14115 return false; 14116 } 14117 })(); 14118 return _isWebGPUSupported; 14119} 14120 14121const renderPriority = ["webgl", "webgpu", "canvas"]; 14122async function autoDetectRenderer(options) { 14123 let preferredOrder = []; 14124 if (options.preference) { 14125 preferredOrder.push(options.preference); 14126 renderPriority.forEach((item) => { 14127 if (item !== options.preference) { 14128 preferredOrder.push(item); 14129 } 14130 }); 14131 } else { 14132 preferredOrder = renderPriority.slice(); 14133 } 14134 let RendererClass; 14135 let finalOptions = {}; 14136 for (let i = 0; i < preferredOrder.length; i++) { 14137 const rendererType = preferredOrder[i]; 14138 if (rendererType === "webgpu" && await isWebGPUSupported()) { 14139 const { WebGPURenderer } = await __vitePreload(async () => { const { WebGPURenderer } = await import('./WebGPURenderer-C0Fiuo6s.js');return { WebGPURenderer }},true ?__vite__mapDeps([55,2,56,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54]):void 0,import.meta.url); 14140 RendererClass = WebGPURenderer; 14141 finalOptions = { ...options, ...options.webgpu }; 14142 break; 14143 } else if (rendererType === "webgl" && isWebGLSupported( 14144 options.failIfMajorPerformanceCaveat ?? AbstractRenderer.defaultOptions.failIfMajorPerformanceCaveat 14145 )) { 14146 const { WebGLRenderer } = await __vitePreload(async () => { const { WebGLRenderer } = await import('./WebGLRenderer-x_6nJWeO.js');return { WebGLRenderer }},true ?__vite__mapDeps([57,2,56,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54]):void 0,import.meta.url); 14147 RendererClass = WebGLRenderer; 14148 finalOptions = { ...options, ...options.webgl }; 14149 break; 14150 } else if (rendererType === "canvas") { 14151 finalOptions = { ...options }; 14152 throw new Error("CanvasRenderer is not yet implemented"); 14153 } 14154 } 14155 delete finalOptions.webgpu; 14156 delete finalOptions.webgl; 14157 if (!RendererClass) { 14158 throw new Error("No available renderer for the current environment"); 14159 } 14160 const renderer = new RendererClass(); 14161 await renderer.init(finalOptions); 14162 return renderer; 14163} 14164 14165const VERSION = "8.15.0"; 14166 14167class ApplicationInitHook { 14168 static init() { 14169 globalThis.__PIXI_APP_INIT__?.(this, VERSION); 14170 } 14171 static destroy() { 14172 } 14173} 14174/** @ignore */ 14175ApplicationInitHook.extension = ExtensionType.Application; 14176class RendererInitHook { 14177 constructor(renderer) { 14178 this._renderer = renderer; 14179 } 14180 init() { 14181 globalThis.__PIXI_RENDERER_INIT__?.(this._renderer, VERSION); 14182 } 14183 destroy() { 14184 this._renderer = null; 14185 } 14186} 14187/** @ignore */ 14188RendererInitHook.extension = { 14189 type: [ 14190 ExtensionType.WebGLSystem, 14191 ExtensionType.WebGPUSystem 14192 ], 14193 name: "initHook", 14194 priority: -10 14195}; 14196 14197const _Application = class _Application { 14198 constructor(...args) { 14199 /** 14200 * The root display container for your application. 14201 * All visual elements should be added to this container or its children. 14202 * @example 14203 * ```js 14204 * // Create a sprite and add it to the stage 14205 * const sprite = Sprite.from('image.png'); 14206 * app.stage.addChild(sprite); 14207 * 14208 * // Create a container for grouping objects 14209 * const container = new Container(); 14210 * app.stage.addChild(container); 14211 * ``` 14212 */ 14213 this.stage = new Container(); 14214 if (args[0] !== void 0) { 14215 deprecation(v8_0_0, "Application constructor options are deprecated, please use Application.init() instead."); 14216 } 14217 } 14218 /** 14219 * Initializes the PixiJS application with the specified options. 14220 * 14221 * This method must be called after creating a new Application instance. 14222 * @param options - Configuration options for the application and renderer 14223 * @returns A promise that resolves when initialization is complete 14224 * @example 14225 * ```js 14226 * const app = new Application(); 14227 * 14228 * // Initialize with custom options 14229 * await app.init({ 14230 * width: 800, 14231 * height: 600, 14232 * backgroundColor: 0x1099bb, 14233 * preference: 'webgl', // or 'webgpu' 14234 * }); 14235 * ``` 14236 */
14237 async init(options) { 14238 options = { ...options }; 14239 this.stage || (this.stage = new Container()); 14240 this.renderer = await autoDetectRenderer(options); 14241 _Application._plugins.forEach((plugin) => { 14242 plugin.init.call(this, options); 14243 }); 14244 } 14245 /** 14246 * Renders the current stage to the screen. 14247 * 14248 * When using the default setup with {@link TickerPlugin} (enabled by default), you typically don't need to call 14249 * this method directly as rendering is handled automatically. 14250 * 14251 * Only use this method if you've disabled the {@link TickerPlugin} or need custom 14252 * render timing control. 14253 * @example 14254 * ```js 14255 * // Example 1: Default setup (TickerPlugin handles rendering) 14256 * const app = new Application(); 14257 * await app.init(); 14258 * // No need to call render() - TickerPlugin handles it 14259 * 14260 * // Example 2: Custom rendering loop (if TickerPlugin is disabled) 14261 * const app = new Application(); 14262 * await app.init({ autoStart: false }); // Disable automatic rendering 14263 * 14264 * function animate() { 14265 * app.render(); 14266 * requestAnimationFrame(animate); 14267 * } 14268 * animate(); 14269 * ``` 14270 */ 14271 render() { 14272 this.renderer.render({ container: this.stage }); 14273 } 14274 /** 14275 * Reference to the renderer's canvas element. This is the HTML element 14276 * that displays your application's graphics. 14277 * @readonly 14278 * @type {HTMLCanvasElement} 14279 * @example 14280 * ```js 14281 * // Create a new application 14282 * const app = new Application(); 14283 * // Initialize the application 14284 * await app.init({...}); 14285 * // Add canvas to the page 14286 * document.body.appendChild(app.canvas); 14287 * 14288 * // Access the canvas directly 14289 * console.log(app.canvas); // HTMLCanvasElement 14290 * ``` 14291 */ 14292 get canvas() { 14293 return this.renderer.canvas; 14294 } 14295 /** 14296 * Reference to the renderer's canvas element. 14297 * @type {HTMLCanvasElement} 14298 * @deprecated since 8.0.0 14299 * @see {@link Application#canvas} 14300 */ 14301 get view() { 14302 deprecation(v8_0_0, "Application.view is deprecated, please use Application.canvas instead."); 14303 return this.renderer.canvas; 14304 } 14305 /** 14306 * Reference to the renderer's screen rectangle. This represents the visible area of your application. 14307 * 14308 * It's commonly used for: 14309 * - Setting filter areas for full-screen effects 14310 * - Defining hit areas for screen-wide interaction 14311 * - Determining the visible bounds of your application 14312 * @readonly 14313 * @example 14314 * ```js 14315 * // Use as filter area for a full-screen effect 14316 * const blurFilter = new BlurFilter(); 14317 * sprite.filterArea = app.screen; 14318 * 14319 * // Use as hit area for screen-wide interaction 14320 * const screenSprite = new Sprite(); 14321 * screenSprite.hitArea = app.screen; 14322 * 14323 * // Get screen dimensions 14324 * console.log(app.screen.width, app.screen.height); 14325 * ``` 14326 * @see {@link Rectangle} For all available properties and methods 14327 */ 14328 get screen() { 14329 return this.renderer.screen; 14330 } 14331 /** 14332 * Destroys the application and all of its resources. 14333 * 14334 * This method should be called when you want to completely 14335 * clean up the application and free all associated memory. 14336 * @param rendererDestroyOptions - Options for destroying the renderer: 14337 * - `false` or `undefined`: Preserves the canvas element (default) 14338 * - `true`: Removes the canvas element 14339 * - `{ removeView: boolean }`: Object with removeView property to control canvas removal 14340 * @param options - Options for destroying the application: 14341 * - `false` or `undefined`: Basic cleanup (default) 14342 * - `true`: Complete cleanup including children 14343 * - Detailed options object: 14344 * - `children`: Remove children 14345 * - `texture`: Destroy textures 14346 * - `textureSource`: Destroy texture sources 14347 * - `context`: Destroy WebGL context 14348 * @example 14349 * ```js 14350 * // Basic cleanup 14351 * app.destroy(); 14352 * 14353 * // Remove canvas and do complete cleanup 14354 * app.destroy(true, true); 14355 * 14356 * // Remove canvas with explicit options 14357 * app.destroy({ removeView: true }, true); 14358 * 14359 * // Detailed cleanup with specific options 14360 * app.destroy( 14361 * { removeView: true }, 14362 * { 14363 * children: true, 14364 * texture: true, 14365 * textureSource: true, 14366 * context: true 14367 * } 14368 * ); 14369 * ``` 14370 * > [!WARNING] After calling destroy, the application instance should no longer be used. 14371 * > All properties will be null and further operations will throw errors. 14372 */ 14373 destroy(rendererDestroyOptions = false, options = false) { 14374 const plugins = _Application._plugins.slice(0); 14375 plugins.reverse(); 14376 plugins.forEach((plugin) => { 14377 plugin.destroy.call(this); 14378 }); 14379 this.stage.destroy(options); 14380 this.stage = null; 14381 this.renderer.destroy(rendererDestroyOptions); 14382 this.renderer = null; 14383 } 14384}; 14385/** 14386 * Collection of installed plugins. 14387 * @internal 14388 */ 14389_Application._plugins = []; 14390let Application = _Application; 14391extensions.handleByList(ExtensionType.Application, Application._plugins); 14392extensions.add(ApplicationInitHook); 14393
14394class AbstractBitmapFont extends EventEmitter { 14395 constructor() { 14396 super(...arguments); 14397 /** The map of characters by character code. */ 14398 this.chars = /* @__PURE__ */ Object.create(null); 14399 /** 14400 * The line-height of the font face in pixels. 14401 * @type {number} 14402 */ 14403 this.lineHeight = 0; 14404 /** 14405 * The name of the font face 14406 * @type {string} 14407 */ 14408 this.fontFamily = ""; 14409 /** The metrics of the font face. */ 14410 this.fontMetrics = { fontSize: 0, ascent: 0, descent: 0 }; 14411 /** 14412 * The offset of the font face from the baseline. 14413 * @type {number} 14414 */ 14415 this.baseLineOffset = 0; 14416 /** The range and type of the distance field for this font. */ 14417 this.distanceField = { type: "none", range: 0 }; 14418 /** The map of base page textures (i.e., sheets of glyphs). */ 14419 this.pages = []; 14420 /** should the fill for this font be applied as a tint to the text. */ 14421 this.applyFillAsTint = true; 14422 /** The size of the font face in pixels. */ 14423 this.baseMeasurementFontSize = 100; 14424 this.baseRenderedFontSize = 100; 14425 } 14426 /** 14427 * The name of the font face. 14428 * @deprecated since 8.0.0 Use `fontFamily` instead. 14429 */ 14430 get font() { 14431 deprecation(v8_0_0, "BitmapFont.font is deprecated, please use BitmapFont.fontFamily instead."); 14432 return this.fontFamily; 14433 } 14434 /** 14435 * The map of base page textures (i.e., sheets of glyphs). 14436 * @deprecated since 8.0.0 Use `pages` instead. 14437 */ 14438 get pageTextures() { 14439 deprecation(v8_0_0, "BitmapFont.pageTextures is deprecated, please use BitmapFont.pages instead."); 14440 return this.pages; 14441 } 14442 /** 14443 * The size of the font face in pixels. 14444 * @deprecated since 8.0.0 Use `fontMetrics.fontSize` instead. 14445 */ 14446 get size() { 14447 deprecation(v8_0_0, "BitmapFont.size is deprecated, please use BitmapFont.fontMetrics.fontSize instead."); 14448 return this.fontMetrics.fontSize; 14449 } 14450 /** 14451 * The kind of distance field for this font or "none". 14452 * @deprecated since 8.0.0 Use `distanceField.type` instead. 14453 */ 14454 get distanceFieldRange() { 14455 deprecation(v8_0_0, "BitmapFont.distanceFieldRange is deprecated, please use BitmapFont.distanceField.range instead."); 14456 return this.distanceField.range; 14457 } 14458 /** 14459 * The range of the distance field in pixels. 14460 * @deprecated since 8.0.0 Use `distanceField.range` instead. 14461 */ 14462 get distanceFieldType() { 14463 deprecation(v8_0_0, "BitmapFont.distanceFieldType is deprecated, please use BitmapFont.distanceField.type instead."); 14464 return this.distanceField.type; 14465 } 14466 destroy(destroyTextures = false) { 14467 this.emit("destroy", this); 14468 this.removeAllListeners(); 14469 for (const i in this.chars) { 14470 this.chars[i].texture?.destroy(); 14471 } 14472 this.chars = null; 14473 if (destroyTextures) { 14474 this.pages.forEach((page) => page.texture.destroy(true)); 14475 this.pages = null; 14476 } 14477 } 14478} 14479 14480/** 14481 * tiny-lru 14482 * 14483 * @copyright 2026 Jason Mulligan <[email protected]> 14484 * @license BSD-3-Clause 14485 * @version 11.4.7 14486 */ 14487/** 14488 * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support. 14489 * Items are automatically evicted when the cache reaches its maximum size, 14490 * removing the least recently used items first. All core operations (get, set, delete) are O(1). 14491 * 14492 * @class LRU 14493 * @example 14494 * // Create a cache with max 100 items 14495 * const cache = new LRU(100); 14496 * cache.set('key1', 'value1'); 14497 * console.log(cache.get('key1')); // 'value1' 14498 * 14499 * @example 14500 * // Create a cache with TTL 14501 * const cache = new LRU(100, 5000); // 5 second TTL 14502 * cache.set('key1', 'value1'); 14503 * // After 5 seconds, key1 will be expired 14504 */ 14505class LRU { 14506 /** 14507 * Creates a new LRU cache instance. 14508 * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation. 14509 * 14510 * @constructor 14511 * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited. 14512 * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration. 14513 * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get(). 14514 * @example 14515 * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access 14516 * @see {@link lru} For parameter validation 14517 * @since 1.0.0 14518 */ 14519 constructor (max = 0, ttl = 0, resetTtl = false) { 14520 this.first = null; 14521 this.items = Object.create(null); 14522 this.last = null; 14523 this.max = max; 14524 this.resetTtl = resetTtl; 14525 this.size = 0; 14526 this.ttl = ttl; 14527 } 14528 14529 /** 14530 * Removes all items from the cache. 14531 * 14532 * @method clear 14533 * @memberof LRU 14534 * @returns {LRU} The LRU instance for method chaining. 14535 * @example 14536 * cache.clear(); 14537 * console.log(cache.size); // 0 14538 * @since 1.0.0 14539 */ 14540 clear () { 14541 this.first = null; 14542 this.items = Object.create(null); 14543 this.last = null; 14544 this.size = 0; 14545 14546 return this; 14547 } 14548 14549 /** 14550 * Removes an item from the cache by key. 14551 * 14552 * @method delete 14553 * @memberof LRU 14554 * @param {string} key - The key of the item to delete. 14555 * @returns {LRU} The LRU instance for method chaining. 14556 * @example 14557 * cache.set('key1', 'value1'); 14558 * cache.delete('key1'); 14559 * console.log(cache.has('key1')); // false 14560 * @see {@link LRU#has} 14561 * @see {@link LRU#clear} 14562 * @since 1.0.0 14563 */ 14564 delete (key) { 14565 if (this.has(key)) { 14566 const item = this.items[key]; 14567 14568 delete this.items[key]; 14569 this.size--; 14570 14571 if (item.prev !== null) { 14572 item.prev.next = item.next; 14573 } 14574 14575 if (item.next !== null) { 14576 item.next.prev = item.prev; 14577 } 14578 14579 if (this.first === item) { 14580 this.first = item.next; 14581 } 14582 14583 if (this.last === item) { 14584 this.last = item.prev; 14585 } 14586 } 14587 14588 return this; 14589 } 14590 14591 /** 14592 * Returns an array of [key, value] pairs for the specified keys.
14593 * Order follows LRU order (least to most recently used). 14594 * 14595 * @method entries 14596 * @memberof LRU 14597 * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. 14598 * @returns {Array<Array<*>>} Array of [key, value] pairs in LRU order. 14599 * @example 14600 * cache.set('a', 1).set('b', 2); 14601 * console.log(cache.entries()); // [['a', 1], ['b', 2]] 14602 * console.log(cache.entries(['a'])); // [['a', 1]] 14603 * @see {@link LRU#keys} 14604 * @see {@link LRU#values} 14605 * @since 11.1.0 14606 */ 14607 entries (keys = this.keys()) { 14608 const result = new Array(keys.length); 14609 for (let i = 0; i < keys.length; i++) { 14610 const key = keys[i]; 14611 result[i] = [key, this.get(key)]; 14612 } 14613 14614 return result; 14615 } 14616 14617 /** 14618 * Removes the least recently used item from the cache. 14619 * 14620 * @method evict 14621 * @memberof LRU 14622 * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. 14623 * @returns {LRU} The LRU instance for method chaining. 14624 * @example 14625 * cache.set('old', 'value').set('new', 'value'); 14626 * cache.evict(); // Removes 'old' item 14627 * @see {@link LRU#setWithEvicted} 14628 * @since 1.0.0 14629 */ 14630 evict (bypass = false) { 14631 if (bypass || this.size > 0) { 14632 const item = this.first; 14633 14634 delete this.items[item.key]; 14635 14636 if (--this.size === 0) { 14637 this.first = null; 14638 this.last = null; 14639 } else { 14640 this.first = item.next; 14641 this.first.prev = null; 14642 } 14643 } 14644 14645 return this; 14646 } 14647 14648 /** 14649 * Returns the expiration timestamp for a given key. 14650 * 14651 * @method expiresAt 14652 * @memberof LRU 14653 * @param {string} key - The key to check expiration for. 14654 * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. 14655 * @example 14656 * const cache = new LRU(100, 5000); // 5 second TTL 14657 * cache.set('key1', 'value1'); 14658 * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now 14659 * @see {@link LRU#get} 14660 * @see {@link LRU#has} 14661 * @since 1.0.0 14662 */ 14663 expiresAt (key) { 14664 let result; 14665 14666 if (this.has(key)) { 14667 result = this.items[key].expiry; 14668 } 14669 14670 return result; 14671 } 14672 14673 /** 14674 * Retrieves a value from the cache by key. Updates the item's position to most recently used. 14675 * 14676 * @method get 14677 * @memberof LRU 14678 * @param {string} key - The key to retrieve. 14679 * @returns {*} The value associated with the key, or undefined if not found or expired. 14680 * @example 14681 * cache.set('key1', 'value1'); 14682 * console.log(cache.get('key1')); // 'value1' 14683 * console.log(cache.get('nonexistent')); // undefined 14684 * @see {@link LRU#set} 14685 * @see {@link LRU#has} 14686 * @since 1.0.0 14687 */ 14688 get (key) { 14689 const item = this.items[key]; 14690 14691 if (item !== undefined) { 14692 // Check TTL only if enabled to avoid unnecessary Date.now() calls 14693 if (this.ttl > 0) { 14694 if (item.expiry <= Date.now()) { 14695 this.delete(key); 14696 14697 return undefined; 14698 } 14699 } 14700 14701 // Fast LRU update without full set() overhead 14702 this.moveToEnd(item); 14703 14704 return item.value; 14705 } 14706 14707 return undefined; 14708 } 14709 14710 /** 14711 * Checks if a key exists in the cache. 14712 * 14713 * @method has 14714 * @memberof LRU 14715 * @param {string} key - The key to check for. 14716 * @returns {boolean} True if the key exists, false otherwise. 14717 * @example 14718 * cache.set('key1', 'value1'); 14719 * console.log(cache.has('key1')); // true 14720 * console.log(cache.has('nonexistent')); // false 14721 * @see {@link LRU#get} 14722 * @see {@link LRU#delete} 14723 * @since 9.0.0 14724 */ 14725 has (key) { 14726 return key in this.items; 14727 } 14728 14729 /** 14730 * Efficiently moves an item to the end of the LRU list (most recently used position). 14731 * This is an internal optimization method that avoids the overhead of the full set() operation 14732 * when only LRU position needs to be updated. 14733 * 14734 * @method moveToEnd 14735 * @memberof LRU 14736 * @param {Object} item - The cache item with prev/next pointers to reposition. 14737 * @private 14738 * @since 11.3.5 14739 */ 14740 moveToEnd (item) { 14741 // If already at the end, nothing to do 14742 if (this.last === item) { 14743 return; 14744 } 14745 14746 // Remove item from current position in the list 14747 if (item.prev !== null) { 14748 item.prev.next = item.next; 14749 } 14750 14751 if (item.next !== null) { 14752 item.next.prev = item.prev; 14753 } 14754 14755 // Update first pointer if this was the first item 14756 if (this.first === item) { 14757 this.first = item.next; 14758 } 14759 14760 // Add item to the end 14761 item.prev = this.last; 14762 item.next = null; 14763 14764 if (this.last !== null) { 14765 this.last.next = item; 14766 } 14767 14768 this.last = item; 14769 14770 // Handle edge case: if this was the only item, it's also first 14771 if (this.first === null) { 14772 this.first = item; 14773 } 14774 } 14775
vendor: 8,707 bytes, lines 14776-15054
14776 /** 14777 * Returns an array of all keys in the cache, ordered from least to most recently used. 14778 * 14779 * @method keys 14780 * @memberof LRU 14781 * @returns {string[]} Array of keys in LRU order. 14782 * @example 14783 * cache.set('a', 1).set('b', 2); 14784 * cache.get('a'); // Move 'a' to most recent 14785 * console.log(cache.keys()); // ['b', 'a'] 14786 * @see {@link LRU#values} 14787 * @see {@link LRU#entries} 14788 * @since 9.0.0 14789 */ 14790 keys () { 14791 const result = new Array(this.size); 14792 let x = this.first; 14793 let i = 0; 14794 14795 while (x !== null) { 14796 result[i++] = x.key; 14797 x = x.next; 14798 } 14799 14800 return result; 14801 } 14802 14803 /** 14804 * Sets a value in the cache and returns any evicted item. 14805 * 14806 * @method setWithEvicted 14807 * @memberof LRU 14808 * @param {string} key - The key to set. 14809 * @param {*} value - The value to store. 14810 * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. 14811 * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. 14812 * @example 14813 * const cache = new LRU(2); 14814 * cache.set('a', 1).set('b', 2); 14815 * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} 14816 * @see {@link LRU#set} 14817 * @see {@link LRU#evict} 14818 * @since 11.3.0 14819 */ 14820 setWithEvicted (key, value, resetTtl = this.resetTtl) { 14821 let evicted = null; 14822 14823 if (this.has(key)) { 14824 this.set(key, value, true, resetTtl); 14825 } else { 14826 if (this.max > 0 && this.size === this.max) { 14827 evicted = {...this.first}; 14828 this.evict(true); 14829 } 14830 14831 let item = this.items[key] = { 14832 expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, 14833 key: key, 14834 prev: this.last, 14835 next: null, 14836 value 14837 }; 14838 14839 if (++this.size === 1) { 14840 this.first = item; 14841 } else { 14842 this.last.next = item; 14843 } 14844 14845 this.last = item; 14846 } 14847 14848 return evicted; 14849 } 14850 14851 /** 14852 * Sets a value in the cache. Updates the item's position to most recently used. 14853 * 14854 * @method set 14855 * @memberof LRU 14856 * @param {string} key - The key to set. 14857 * @param {*} value - The value to store. 14858 * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. 14859 * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. 14860 * @returns {LRU} The LRU instance for method chaining. 14861 * @example 14862 * cache.set('key1', 'value1') 14863 * .set('key2', 'value2') 14864 * .set('key3', 'value3'); 14865 * @see {@link LRU#get} 14866 * @see {@link LRU#setWithEvicted} 14867 * @since 1.0.0 14868 */ 14869 set (key, value, bypass = false, resetTtl = this.resetTtl) { 14870 let item = this.items[key]; 14871 14872 if (bypass || item !== undefined) { 14873 // Existing item: update value and position 14874 item.value = value; 14875 14876 if (bypass === false && resetTtl) { 14877 item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; 14878 } 14879 14880 // Always move to end, but the bypass parameter affects TTL reset behavior 14881 this.moveToEnd(item); 14882 } else { 14883 // New item: check for eviction and create 14884 if (this.max > 0 && this.size === this.max) { 14885 this.evict(true); 14886 } 14887 14888 item = this.items[key] = { 14889 expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, 14890 key: key, 14891 prev: this.last, 14892 next: null, 14893 value 14894 }; 14895 14896 if (++this.size === 1) { 14897 this.first = item; 14898 } else { 14899 this.last.next = item; 14900 } 14901 14902 this.last = item; 14903 } 14904 14905 return this; 14906 } 14907 14908 /** 14909 * Returns an array of all values in the cache for the specified keys. 14910 * Order follows LRU order (least to most recently used). 14911 * 14912 * @method values 14913 * @memberof LRU 14914 * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. 14915 * @returns {Array<*>} Array of values corresponding to the keys in LRU order. 14916 * @example 14917 * cache.set('a', 1).set('b', 2); 14918 * console.log(cache.values()); // [1, 2] 14919 * console.log(cache.values(['a'])); // [1] 14920 * @see {@link LRU#keys} 14921 * @see {@link LRU#entries} 14922 * @since 11.1.0 14923 */ 14924 values (keys = this.keys()) { 14925 const result = new Array(keys.length); 14926 for (let i = 0; i < keys.length; i++) { 14927 result[i] = this.get(keys[i]); 14928 } 14929 14930 return result; 14931 } 14932} 14933 14934/** 14935 * Factory function to create a new LRU cache instance with parameter validation. 14936 * 14937 * @function lru 14938 * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. 14939 * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. 14940 * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get(). 14941 * @returns {LRU} A new LRU cache instance. 14942 * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). 14943 * @example 14944 * // Create cache with factory function 14945 * const cache = lru(100, 5000, true); 14946 * cache.set('key', 'value'); 14947 * 14948 * @example 14949 * // Error handling 14950 * try { 14951 * const cache = lru(-1); // Invalid max 14952 * } catch (error) { 14953 * console.error(error.message); // "Invalid max value" 14954 * } 14955 * @see {@link LRU} 14956 * @since 1.0.0 14957 */ 14958function lru (max = 1000, ttl = 0, resetTtl = false) { 14959 if (isNaN(max) || max < 0) { 14960 throw new TypeError("Invalid max value"); 14961 } 14962 14963 if (isNaN(ttl) || ttl < 0) { 14964 throw new TypeError("Invalid ttl value"); 14965 } 14966 14967 if (typeof resetTtl !== "boolean") { 14968 throw new TypeError("Invalid resetTtl value"); 14969 } 14970 14971 return new LRU(max, ttl, resetTtl); 14972} 14973 14974const genericFontFamilies = [ 14975 "serif", 14976 "sans-serif", 14977 "monospace", 14978 "cursive", 14979 "fantasy", 14980 "system-ui" 14981]; 14982function fontStringFromTextStyle(style) { 14983 const fontSizeString = typeof style.fontSize === "number" ? `${style.fontSize}px` : style.fontSize; 14984 let fontFamilies = style.fontFamily; 14985 if (!Array.isArray(style.fontFamily)) { 14986 fontFamilies = style.fontFamily.split(","); 14987 } 14988 for (let i = fontFamilies.length - 1; i >= 0; i--) { 14989 let fontFamily = fontFamilies[i].trim(); 14990 if (!/([\"\'])[^\'\"]+\1/.test(fontFamily) && !genericFontFamilies.includes(fontFamily)) { 14991 fontFamily = `"${fontFamily}"`; 14992 } 14993 fontFamilies[i] = fontFamily; 14994 } 14995 return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(",")}`; 14996} 14997 14998const contextSettings = { 14999 // TextMetrics requires getImageData readback for measuring fonts. 15000 willReadFrequently: true 15001}; 15002const _CanvasTextMetrics = class _CanvasTextMetrics { 15003 /** 15004 * Checking that we can use modern canvas 2D API. 15005 * 15006 * Note: This is an unstable API, Chrome < 94 use `textLetterSpacing`, later versions use `letterSpacing`. 15007 * @see TextMetrics.experimentalLetterSpacing 15008 * @see https://developer.mozilla.org/en-US/docs/Web/API/ICanvasRenderingContext2D/letterSpacing 15009 * @see https://developer.chrome.com/origintrials/#/view_trial/3585991203293757441 15010 */ 15011 static get experimentalLetterSpacingSupported() { 15012 let result = _CanvasTextMetrics._experimentalLetterSpacingSupported; 15013 if (result === void 0) { 15014 const proto = DOMAdapter.get().getCanvasRenderingContext2D().prototype; 15015 result = _CanvasTextMetrics._experimentalLetterSpacingSupported = "letterSpacing" in proto || "textLetterSpacing" in proto; 15016 } 15017 return result; 15018 } 15019 /** 15020 * @param text - the text that was measured 15021 * @param style - the style that was measured 15022 * @param width - the measured width of the text 15023 * @param height - the measured height of the text 15024 * @param lines - an array of the lines of text broken by new lines and wrapping if specified in style 15025 * @param lineWidths - an array of the line widths for each line matched to `lines` 15026 * @param lineHeight - the measured line height for this style 15027 * @param maxLineWidth - the maximum line width for all measured lines 15028 * @param {FontMetrics} fontProperties - the font properties object from TextMetrics.measureFont 15029 */ 15030 constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) { 15031 this.text = text; 15032 this.style = style; 15033 this.width = width; 15034 this.height = height; 15035 this.lines = lines; 15036 this.lineWidths = lineWidths; 15037 this.lineHeight = lineHeight; 15038 this.maxLineWidth = maxLineWidth; 15039 this.fontProperties = fontProperties; 15040 } 15041 /** 15042 * Measures the supplied string of text and returns a Rectangle. 15043 * @param text - The text to measure. 15044 * @param style - The text style to use for measuring 15045 * @param canvas - optional specification of the canvas to use for measuring. 15046 * @param wordWrap 15047 * @returns Measured width and height of the text. 15048 */ 15049 static measureText(text = " ", style, canvas = _CanvasTextMetrics._canvas, wordWrap = style.wordWrap) { 15050 const textKey = `${text}-${style.styleKey}-wordWrap-${wordWrap}`; 15051 if (_CanvasTextMetrics._measurementCache.has(textKey)) { 15052 return _CanvasTextMetrics._measurementCache.get(textKey); 15053 } 15054 const font = fontStringFromTextStyle(style);
15055 const fontProperties = _CanvasTextMetrics.measureFont(font); 15056 if (fontProperties.fontSize === 0) { 15057 fontProperties.fontSize = style.fontSize; 15058 fontProperties.ascent = style.fontSize; 15059 } 15060 const context = _CanvasTextMetrics.__context; 15061 context.font = font; 15062 const outputText = wordWrap ? _CanvasTextMetrics._wordWrap(text, style, canvas) : text; 15063 const lines = outputText.split(/(?:\r\n|\r|\n)/); 15064 const lineWidths = new Array(lines.length); 15065 let maxLineWidth = 0; 15066 for (let i = 0; i < lines.length; i++) { 15067 const lineWidth = _CanvasTextMetrics._measureText(lines[i], style.letterSpacing, context); 15068 lineWidths[i] = lineWidth; 15069 maxLineWidth = Math.max(maxLineWidth, lineWidth); 15070 } 15071 const strokeWidth = style._stroke?.width || 0; 15072 let width = maxLineWidth + strokeWidth; 15073 if (style.dropShadow) { 15074 width += style.dropShadow.distance; 15075 } 15076 const lineHeight = style.lineHeight || fontProperties.fontSize; 15077 let height = Math.max(lineHeight, fontProperties.fontSize + strokeWidth) + (lines.length - 1) * (lineHeight + style.leading); 15078 if (style.dropShadow) { 15079 height += style.dropShadow.distance; 15080 } 15081 const measurements = new _CanvasTextMetrics( 15082 text, 15083 style, 15084 width, 15085 height, 15086 lines, 15087 lineWidths, 15088 lineHeight + style.leading, 15089 maxLineWidth, 15090 fontProperties 15091 ); 15092 _CanvasTextMetrics._measurementCache.set(textKey, measurements); 15093 return measurements; 15094 } 15095 static _measureText(text, letterSpacing, context) { 15096 let useExperimentalLetterSpacing = false; 15097 if (_CanvasTextMetrics.experimentalLetterSpacingSupported) { 15098 if (_CanvasTextMetrics.experimentalLetterSpacing) { 15099 context.letterSpacing = `${letterSpacing}px`; 15100 context.textLetterSpacing = `${letterSpacing}px`; 15101 useExperimentalLetterSpacing = true; 15102 } else { 15103 context.letterSpacing = "0px"; 15104 context.textLetterSpacing = "0px"; 15105 } 15106 } 15107 const metrics = context.measureText(text); 15108 let metricWidth = metrics.width; 15109 const actualBoundingBoxLeft = -metrics.actualBoundingBoxLeft; 15110 const actualBoundingBoxRight = metrics.actualBoundingBoxRight; 15111 let boundsWidth = actualBoundingBoxRight - actualBoundingBoxLeft; 15112 if (metricWidth > 0) { 15113 if (useExperimentalLetterSpacing) { 15114 metricWidth -= letterSpacing; 15115 boundsWidth -= letterSpacing; 15116 } else { 15117 const val = (_CanvasTextMetrics.graphemeSegmenter(text).length - 1) * letterSpacing; 15118 metricWidth += val; 15119 boundsWidth += val; 15120 } 15121 } 15122 return Math.max(metricWidth, boundsWidth); 15123 } 15124 /** 15125 * Applies newlines to a string to have it optimally fit into the horizontal 15126 * bounds set by the Text object's wordWrapWidth property. 15127 * @param text - String to apply word wrapping to 15128 * @param style - the style to use when wrapping 15129 * @param canvas - optional specification of the canvas to use for measuring. 15130 * @returns New string with new lines applied where required 15131 */ 15132 static _wordWrap(text, style, canvas = _CanvasTextMetrics._canvas) { 15133 const context = canvas.getContext("2d", contextSettings); 15134 let width = 0; 15135 let line = ""; 15136 let lines = ""; 15137 const cache = /* @__PURE__ */ Object.create(null); 15138 const { letterSpacing, whiteSpace } = style; 15139 const collapseSpaces = _CanvasTextMetrics._collapseSpaces(whiteSpace); 15140 const collapseNewlines = _CanvasTextMetrics._collapseNewlines(whiteSpace); 15141 let canPrependSpaces = !collapseSpaces; 15142 const wordWrapWidth = style.wordWrapWidth + letterSpacing; 15143 const tokens = _CanvasTextMetrics._tokenize(text); 15144 for (let i = 0; i < tokens.length; i++) { 15145 let token = tokens[i]; 15146 if (_CanvasTextMetrics._isNewline(token)) { 15147 if (!collapseNewlines) { 15148 lines += _CanvasTextMetrics._addLine(line); 15149 canPrependSpaces = !collapseSpaces; 15150 line = ""; 15151 width = 0; 15152 continue; 15153 } 15154 token = " "; 15155 } 15156 if (collapseSpaces) { 15157 const currIsBreakingSpace = _CanvasTextMetrics.isBreakingSpace(token); 15158 const lastIsBreakingSpace = _CanvasTextMetrics.isBreakingSpace(line[line.length - 1]); 15159 if (currIsBreakingSpace && lastIsBreakingSpace) { 15160 continue; 15161 } 15162 } 15163 const tokenWidth = _CanvasTextMetrics._getFromCache(token, letterSpacing, cache, context); 15164 if (tokenWidth > wordWrapWidth) { 15165 if (line !== "") { 15166 lines += _CanvasTextMetrics._addLine(line); 15167 line = ""; 15168 width = 0; 15169 } 15170 if (_CanvasTextMetrics.canBreakWords(token, style.breakWords)) { 15171 const characters = _CanvasTextMetrics.wordWrapSplit(token); 15172 for (let j = 0; j < characters.length; j++) { 15173 let char = characters[j]; 15174 let lastChar = char; 15175 let k = 1; 15176 while (characters[j + k]) { 15177 const nextChar = characters[j + k]; 15178 if (!_CanvasTextMetrics.canBreakChars(lastChar, nextChar, token, j, style.breakWords)) { 15179 char += nextChar; 15180 } else { 15181 break; 15182 } 15183 lastChar = nextChar; 15184 k++; 15185 } 15186 j += k - 1; 15187 const characterWidth = _CanvasTextMetrics._getFromCache(char, letterSpacing, cache, context); 15188 if (characterWidth + width > wordWrapWidth) { 15189 lines += _CanvasTextMetrics._addLine(line); 15190 canPrependSpaces = false;
15191 line = ""; 15192 width = 0; 15193 } 15194 line += char; 15195 width += characterWidth; 15196 } 15197 } else { 15198 if (line.length > 0) { 15199 lines += _CanvasTextMetrics._addLine(line); 15200 line = ""; 15201 width = 0; 15202 } 15203 const isLastToken = i === tokens.length - 1; 15204 lines += _CanvasTextMetrics._addLine(token, !isLastToken); 15205 canPrependSpaces = false; 15206 line = ""; 15207 width = 0; 15208 } 15209 } else { 15210 if (tokenWidth + width > wordWrapWidth) { 15211 canPrependSpaces = false; 15212 lines += _CanvasTextMetrics._addLine(line); 15213 line = ""; 15214 width = 0; 15215 } 15216 if (line.length > 0 || !_CanvasTextMetrics.isBreakingSpace(token) || canPrependSpaces) { 15217 line += token; 15218 width += tokenWidth; 15219 } 15220 } 15221 } 15222 lines += _CanvasTextMetrics._addLine(line, false); 15223 return lines; 15224 } 15225 /** 15226 * Convenience function for logging each line added during the wordWrap method. 15227 * @param line - The line of text to add 15228 * @param newLine - Add new line character to end 15229 * @returns A formatted line 15230 */ 15231 static _addLine(line, newLine = true) { 15232 line = _CanvasTextMetrics._trimRight(line); 15233 line = newLine ? `${line} 15234` : line; 15235 return line; 15236 } 15237 /** 15238 * Gets & sets the widths of calculated characters in a cache object 15239 * @param key - The key 15240 * @param letterSpacing - The letter spacing 15241 * @param cache - The cache 15242 * @param context - The canvas context 15243 * @returns The from cache. 15244 */ 15245 static _getFromCache(key, letterSpacing, cache, context) { 15246 let width = cache[key]; 15247 if (typeof width !== "number") { 15248 width = _CanvasTextMetrics._measureText(key, letterSpacing, context) + letterSpacing; 15249 cache[key] = width; 15250 } 15251 return width; 15252 } 15253 /** 15254 * Determines whether we should collapse breaking spaces. 15255 * @param whiteSpace - The TextStyle property whiteSpace 15256 * @returns Should collapse 15257 */ 15258 static _collapseSpaces(whiteSpace) { 15259 return whiteSpace === "normal" || whiteSpace === "pre-line"; 15260 } 15261 /** 15262 * Determines whether we should collapse newLine chars. 15263 * @param whiteSpace - The white space 15264 * @returns should collapse 15265 */ 15266 static _collapseNewlines(whiteSpace) { 15267 return whiteSpace === "normal"; 15268 } 15269 /** 15270 * Trims breaking whitespaces from string. 15271 * @param text - The text 15272 * @returns Trimmed string 15273 */ 15274 static _trimRight(text) { 15275 if (typeof text !== "string") { 15276 return ""; 15277 } 15278 for (let i = text.length - 1; i >= 0; i--) { 15279 const char = text[i]; 15280 if (!_CanvasTextMetrics.isBreakingSpace(char)) { 15281 break; 15282 } 15283 text = text.slice(0, -1); 15284 } 15285 return text; 15286 } 15287 /** 15288 * Determines if char is a newline. 15289 * @param char - The character 15290 * @returns True if newline, False otherwise. 15291 */ 15292 static _isNewline(char) { 15293 if (typeof char !== "string") { 15294 return false; 15295 } 15296 return _CanvasTextMetrics._newlines.includes(char.charCodeAt(0)); 15297 } 15298 /** 15299 * Determines if char is a breaking whitespace. 15300 * 15301 * It allows one to determine whether char should be a breaking whitespace 15302 * For example certain characters in CJK langs or numbers. 15303 * It must return a boolean. 15304 * @param char - The character 15305 * @param [_nextChar] - The next character 15306 * @returns True if whitespace, False otherwise. 15307 */ 15308 static isBreakingSpace(char, _nextChar) { 15309 if (typeof char !== "string") { 15310 return false; 15311 } 15312 return _CanvasTextMetrics._breakingSpaces.includes(char.charCodeAt(0)); 15313 } 15314 /** 15315 * Splits a string into words, breaking-spaces and newLine characters 15316 * @param text - The text 15317 * @returns A tokenized array 15318 */ 15319 static _tokenize(text) { 15320 const tokens = []; 15321 let token = ""; 15322 if (typeof text !== "string") { 15323 return tokens; 15324 } 15325 for (let i = 0; i < text.length; i++) { 15326 const char = text[i]; 15327 const nextChar = text[i + 1]; 15328 if (_CanvasTextMetrics.isBreakingSpace(char, nextChar) || _CanvasTextMetrics._isNewline(char)) { 15329 if (token !== "") { 15330 tokens.push(token); 15331 token = ""; 15332 } 15333 if (char === "\r" && nextChar === "\n") { 15334 tokens.push("\r\n"); 15335 i++; 15336 } else { 15337 tokens.push(char); 15338 } 15339 continue; 15340 } 15341 token += char; 15342 } 15343 if (token !== "") { 15344 tokens.push(token); 15345 } 15346 return tokens; 15347 } 15348 /** 15349 * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior. 15350 * 15351 * It allows one to customise which words should break 15352 * Examples are if the token is CJK or numbers. 15353 * It must return a boolean. 15354 * @param _token - The token 15355 * @param breakWords - The style attr break words 15356 * @returns Whether to break word or not 15357 */ 15358 static canBreakWords(_token, breakWords) { 15359 return breakWords; 15360 } 15361 /** 15362 * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior. 15363 * 15364 * It allows one to determine whether a pair of characters 15365 * should be broken by newlines 15366 * For example certain characters in CJK langs or numbers. 15367 * It must return a boolean. 15368 * @param _char - The character 15369 * @param _nextChar - The next character 15370 * @param _token - The token/word the characters are from 15371 * @param _index - The index in the token of the char 15372 * @param _breakWords - The style attr break words 15373 * @returns whether to break word or not 15374 */ 15375 static canBreakChars(_char, _nextChar, _token, _index, _breakWords) { 15376 return true; 15377 } 15378 /** 15379 * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior. 15380 * 15381 * It is called when a token (usually a word) has to be split into separate pieces 15382 * in order to determine the point to break a word. 15383 * It must return an array of characters.
15384 * @param token - The token to split 15385 * @returns The characters of the token 15386 * @see CanvasTextMetrics.graphemeSegmenter 15387 */ 15388 static wordWrapSplit(token) { 15389 return _CanvasTextMetrics.graphemeSegmenter(token); 15390 } 15391 /** 15392 * Calculates the ascent, descent and fontSize of a given font-style 15393 * @param font - String representing the style of the font 15394 * @returns Font properties object 15395 */ 15396 static measureFont(font) { 15397 if (_CanvasTextMetrics._fonts[font]) { 15398 return _CanvasTextMetrics._fonts[font]; 15399 } 15400 const context = _CanvasTextMetrics._context; 15401 context.font = font; 15402 const metrics = context.measureText(_CanvasTextMetrics.METRICS_STRING + _CanvasTextMetrics.BASELINE_SYMBOL); 15403 const properties = { 15404 ascent: metrics.actualBoundingBoxAscent, 15405 descent: metrics.actualBoundingBoxDescent, 15406 fontSize: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent 15407 }; 15408 _CanvasTextMetrics._fonts[font] = properties; 15409 return properties; 15410 } 15411 /** 15412 * Clear font metrics in metrics cache. 15413 * @param {string} [font] - font name. If font name not set then clear cache for all fonts. 15414 */ 15415 static clearMetrics(font = "") { 15416 if (font) { 15417 delete _CanvasTextMetrics._fonts[font]; 15418 } else { 15419 _CanvasTextMetrics._fonts = {}; 15420 } 15421 } 15422 /** 15423 * Cached canvas element for measuring text 15424 * TODO: this should be private, but isn't because of backward compat, will fix later. 15425 * @ignore 15426 */ 15427 static get _canvas() { 15428 if (!_CanvasTextMetrics.__canvas) { 15429 let canvas; 15430 try { 15431 const c = new OffscreenCanvas(0, 0); 15432 const context = c.getContext("2d", contextSettings); 15433 if (context?.measureText) { 15434 _CanvasTextMetrics.__canvas = c; 15435 return c; 15436 } 15437 canvas = DOMAdapter.get().createCanvas(); 15438 } catch (_cx) { 15439 canvas = DOMAdapter.get().createCanvas(); 15440 } 15441 canvas.width = canvas.height = 10; 15442 _CanvasTextMetrics.__canvas = canvas; 15443 } 15444 return _CanvasTextMetrics.__canvas; 15445 } 15446 /** 15447 * TODO: this should be private, but isn't because of backward compat, will fix later. 15448 * @ignore 15449 */ 15450 static get _context() { 15451 if (!_CanvasTextMetrics.__context) { 15452 _CanvasTextMetrics.__context = _CanvasTextMetrics._canvas.getContext("2d", contextSettings); 15453 } 15454 return _CanvasTextMetrics.__context; 15455 } 15456}; 15457/** 15458 * String used for calculate font metrics. 15459 * These characters are all tall to help calculate the height required for text. 15460 */ 15461_CanvasTextMetrics.METRICS_STRING = "|\xC9q\xC5"; 15462/** Baseline symbol for calculate font metrics. */ 15463_CanvasTextMetrics.BASELINE_SYMBOL = "M"; 15464/** Baseline multiplier for calculate font metrics. */ 15465_CanvasTextMetrics.BASELINE_MULTIPLIER = 1.4; 15466/** Height multiplier for setting height of canvas to calculate font metrics. */ 15467_CanvasTextMetrics.HEIGHT_MULTIPLIER = 2; 15468/** 15469 * A Unicode "character", or "grapheme cluster", can be composed of multiple Unicode code points, 15470 * such as letters with diacritical marks (e.g. `'\u0065\u0301'`, letter e with acute) 15471 * or emojis with modifiers (e.g. `'\uD83E\uDDD1\u200D\uD83D\uDCBB'`, technologist). 15472 * The new `Intl.Segmenter` API in ES2022 can split the string into grapheme clusters correctly. If it is not available, 15473 * PixiJS will fallback to use the iterator of String, which can only spilt the string into code points. 15474 * If you want to get full functionality in environments that don't support `Intl.Segmenter` (such as Firefox), 15475 * you can use other libraries such as [grapheme-splitter]{@link https://www.npmjs.com/package/grapheme-splitter} 15476 * or [graphemer]{@link https://www.npmjs.com/package/graphemer} to create a polyfill. Since these libraries can be 15477 * relatively large in size to handle various Unicode grapheme clusters properly, PixiJS won't use them directly. 15478 */ 15479_CanvasTextMetrics.graphemeSegmenter = (() => { 15480 if (typeof Intl?.Segmenter === "function") { 15481 const segmenter = new Intl.Segmenter(); 15482 return (s) => { 15483 const segments = segmenter.segment(s); 15484 const result = []; 15485 let i = 0; 15486 for (const segment of segments) { 15487 result[i++] = segment.segment; 15488 } 15489 return result; 15490 }; 15491 } 15492 return (s) => [...s]; 15493})(); 15494/** 15495 * New rendering behavior for letter-spacing which uses Chrome's new native API. This will 15496 * lead to more accurate letter-spacing results because it does not try to manually draw 15497 * each character. However, this Chrome API is experimental and may not serve all cases yet. 15498 * @see TextMetrics.experimentalLetterSpacingSupported 15499 */ 15500_CanvasTextMetrics.experimentalLetterSpacing = false;
15501/** Cache of {@link TextMetrics.FontMetrics} objects. */ 15502_CanvasTextMetrics._fonts = {}; 15503/** Cache of new line chars. */ 15504_CanvasTextMetrics._newlines = [ 15505 10, 15506 // line feed 15507 13 15508 // carriage return 15509]; 15510/** Cache of breaking spaces. */ 15511_CanvasTextMetrics._breakingSpaces = [ 15512 9, 15513 // character tabulation 15514 32, 15515 // space 15516 8192, 15517 // en quad 15518 8193, 15519 // em quad 15520 8194, 15521 // en space 15522 8195, 15523 // em space 15524 8196, 15525 // three-per-em space 15526 8197, 15527 // four-per-em space 15528 8198, 15529 // six-per-em space 15530 8200, 15531 // punctuation space 15532 8201, 15533 // thin space 15534 8202, 15535 // hair space 15536 8287, 15537 // medium mathematical space 15538 12288 15539 // ideographic space 15540]; 15541/** Cache for measured text metrics */ 15542_CanvasTextMetrics._measurementCache = lru(1e3); 15543let CanvasTextMetrics = _CanvasTextMetrics; 15544 15545const emptyColorStops = [{ offset: 0, color: "white" }, { offset: 1, color: "black" }]; 15546const _FillGradient = class _FillGradient { 15547 constructor(...args) { 15548 /** 15549 * Unique identifier for this gradient instance 15550 * @internal 15551 */ 15552 this.uid = uid("fillGradient"); 15553 /** 15554 * Internal tick counter to track changes in the gradient. 15555 * This is used to invalidate the gradient when the texture changes. 15556 * @internal 15557 */ 15558 this._tick = 0; 15559 /** Type of gradient - currently only supports 'linear' */ 15560 this.type = "linear"; 15561 /** Array of color stops defining the gradient */ 15562 this.colorStops = []; 15563 let options = ensureGradientOptions(args); 15564 const defaults = options.type === "radial" ? _FillGradient.defaultRadialOptions : _FillGradient.defaultLinearOptions; 15565 options = { ...defaults, ...definedProps(options) }; 15566 this._textureSize = options.textureSize; 15567 this._wrapMode = options.wrapMode; 15568 if (options.type === "radial") { 15569 this.center = options.center; 15570 this.outerCenter = options.outerCenter ?? this.center; 15571 this.innerRadius = options.innerRadius; 15572 this.outerRadius = options.outerRadius; 15573 this.scale = options.scale; 15574 this.rotation = options.rotation; 15575 } else { 15576 this.start = options.start; 15577 this.end = options.end; 15578 } 15579 this.textureSpace = options.textureSpace; 15580 this.type = options.type; 15581 options.colorStops.forEach((stop) => { 15582 this.addColorStop(stop.offset, stop.color); 15583 }); 15584 } 15585 /** 15586 * Adds a color stop to the gradient 15587 * @param offset - Position of the stop (0-1) 15588 * @param color - Color of the stop 15589 * @returns This gradient instance for chaining 15590 */ 15591 addColorStop(offset, color) { 15592 this.colorStops.push({ offset, color: Color.shared.setValue(color).toHexa() }); 15593 return this; 15594 } 15595 /** 15596 * Builds the internal texture and transform for the gradient. 15597 * Called automatically when the gradient is first used. 15598 * @internal 15599 */ 15600 buildLinearGradient() { 15601 if (this.texture) 15602 return; 15603 let { x: x0, y: y0 } = this.start; 15604 let { x: x1, y: y1 } = this.end; 15605 let dx = x1 - x0; 15606 let dy = y1 - y0; 15607 const flip = dx < 0 || dy < 0; 15608 if (this._wrapMode === "clamp-to-edge") { 15609 if (dx < 0) { 15610 const temp = x0; 15611 x0 = x1; 15612 x1 = temp; 15613 dx *= -1; 15614 } 15615 if (dy < 0) { 15616 const temp = y0; 15617 y0 = y1; 15618 y1 = temp; 15619 dy *= -1; 15620 } 15621 } 15622 const colorStops = this.colorStops.length ? this.colorStops : emptyColorStops; 15623 const defaultSize = this._textureSize; 15624 const { canvas, context } = getCanvas(defaultSize, 1); 15625 const gradient = !flip ? context.createLinearGradient(0, 0, this._textureSize, 0) : context.createLinearGradient(this._textureSize, 0, 0, 0); 15626 addColorStops(gradient, colorStops); 15627 context.fillStyle = gradient; 15628 context.fillRect(0, 0, defaultSize, 1); 15629 this.texture = new Texture({ 15630 source: new ImageSource({ 15631 resource: canvas, 15632 addressMode: this._wrapMode 15633 }) 15634 }); 15635 const dist = Math.sqrt(dx * dx + dy * dy); 15636 const angle = Math.atan2(dy, dx); 15637 const m = new Matrix(); 15638 m.scale(dist / defaultSize, 1); 15639 m.rotate(angle); 15640 m.translate(x0, y0); 15641 if (this.textureSpace === "local") { 15642 m.scale(defaultSize, defaultSize); 15643 } 15644 this.transform = m; 15645 } 15646 /** 15647 * Builds the internal texture and transform for the gradient. 15648 * Called automatically when the gradient is first used. 15649 * @internal 15650 */ 15651 buildGradient() { 15652 if (!this.texture) 15653 this._tick++;
15654 if (this.type === "linear") { 15655 this.buildLinearGradient(); 15656 } else { 15657 this.buildRadialGradient(); 15658 } 15659 } 15660 /** 15661 * Builds the internal texture and transform for the radial gradient. 15662 * Called automatically when the gradient is first used. 15663 * @internal 15664 */ 15665 buildRadialGradient() { 15666 if (this.texture) 15667 return; 15668 const colorStops = this.colorStops.length ? this.colorStops : emptyColorStops; 15669 const defaultSize = this._textureSize; 15670 const { canvas, context } = getCanvas(defaultSize, defaultSize); 15671 const { x: x0, y: y0 } = this.center; 15672 const { x: x1, y: y1 } = this.outerCenter; 15673 const r0 = this.innerRadius; 15674 const r1 = this.outerRadius; 15675 const ox = x1 - r1; 15676 const oy = y1 - r1; 15677 const scale = defaultSize / (r1 * 2); 15678 const cx = (x0 - ox) * scale; 15679 const cy = (y0 - oy) * scale; 15680 const gradient = context.createRadialGradient( 15681 cx, 15682 cy, 15683 r0 * scale, 15684 (x1 - ox) * scale, 15685 (y1 - oy) * scale, 15686 r1 * scale 15687 ); 15688 addColorStops(gradient, colorStops); 15689 context.fillStyle = colorStops[colorStops.length - 1].color; 15690 context.fillRect(0, 0, defaultSize, defaultSize); 15691 context.fillStyle = gradient; 15692 context.translate(cx, cy); 15693 context.rotate(this.rotation); 15694 context.scale(1, this.scale); 15695 context.translate(-cx, -cy); 15696 context.fillRect(0, 0, defaultSize, defaultSize); 15697 this.texture = new Texture({ 15698 source: new ImageSource({ 15699 resource: canvas, 15700 addressMode: this._wrapMode 15701 }) 15702 }); 15703 const m = new Matrix(); 15704 m.scale(1 / scale, 1 / scale); 15705 m.translate(ox, oy); 15706 if (this.textureSpace === "local") { 15707 m.scale(defaultSize, defaultSize); 15708 } 15709 this.transform = m; 15710 } 15711 /** Destroys the gradient, releasing resources. This will also destroy the internal texture. */ 15712 destroy() { 15713 this.texture?.destroy(true); 15714 this.texture = null; 15715 this.transform = null; 15716 this.colorStops = []; 15717 this.start = null; 15718 this.end = null; 15719 this.center = null; 15720 this.outerCenter = null; 15721 } 15722 /** 15723 * Returns a unique key for this gradient instance. 15724 * This key is used for caching and texture management. 15725 * @returns {string} Unique key for the gradient 15726 */ 15727 get styleKey() { 15728 return `fill-gradient-${this.uid}-${this._tick}`; 15729 } 15730}; 15731/** Default options for creating a gradient fill */ 15732_FillGradient.defaultLinearOptions = { 15733 start: { x: 0, y: 0 }, 15734 end: { x: 0, y: 1 }, 15735 colorStops: [], 15736 textureSpace: "local", 15737 type: "linear", 15738 textureSize: 256, 15739 wrapMode: "clamp-to-edge" 15740}; 15741/** Default options for creating a radial gradient fill */ 15742_FillGradient.defaultRadialOptions = { 15743 center: { x: 0.5, y: 0.5 }, 15744 innerRadius: 0, 15745 outerRadius: 0.5, 15746 colorStops: [], 15747 scale: 1, 15748 textureSpace: "local", 15749 type: "radial", 15750 textureSize: 256, 15751 wrapMode: "clamp-to-edge" 15752}; 15753let FillGradient = _FillGradient; 15754function addColorStops(gradient, colorStops) { 15755 for (let i = 0; i < colorStops.length; i++) { 15756 const stop = colorStops[i]; 15757 gradient.addColorStop(stop.offset, stop.color); 15758 } 15759} 15760function getCanvas(width, height) { 15761 const canvas = DOMAdapter.get().createCanvas(width, height); 15762 const context = canvas.getContext("2d"); 15763 return { canvas, context }; 15764} 15765function ensureGradientOptions(args) { 15766 let options = args[0] ?? {}; 15767 if (typeof options === "number" || args[1]) { 15768 deprecation("8.5.2", `use options object instead`); 15769 options = { 15770 type: "linear", 15771 start: { x: args[0], y: args[1] }, 15772 end: { x: args[2], y: args[3] }, 15773 textureSpace: args[4], 15774 textureSize: args[5] ?? FillGradient.defaultLinearOptions.textureSize 15775 }; 15776 } 15777 return options; 15778} 15779 15780const repetitionMap = { 15781 repeat: { 15782 addressModeU: "repeat", 15783 addressModeV: "repeat" 15784 }, 15785 "repeat-x": { 15786 addressModeU: "repeat", 15787 addressModeV: "clamp-to-edge" 15788 }, 15789 "repeat-y": { 15790 addressModeU: "clamp-to-edge", 15791 addressModeV: "repeat" 15792 }, 15793 "no-repeat": { 15794 addressModeU: "clamp-to-edge", 15795 addressModeV: "clamp-to-edge" 15796 } 15797}; 15798class FillPattern { 15799 constructor(texture, repetition) { 15800 /** 15801 * unique id for this fill pattern 15802 * @internal 15803 */ 15804 this.uid = uid("fillPattern"); 15805 /** 15806 * Internal tick counter to track changes in the pattern.
15807 * This is used to invalidate the pattern when the texture or transform changes. 15808 * @internal 15809 */ 15810 this._tick = 0; 15811 /** The transform matrix applied to the pattern */ 15812 this.transform = new Matrix(); 15813 this.texture = texture; 15814 this.transform.scale( 15815 1 / texture.frame.width, 15816 1 / texture.frame.height 15817 ); 15818 if (repetition) { 15819 texture.source.style.addressModeU = repetitionMap[repetition].addressModeU; 15820 texture.source.style.addressModeV = repetitionMap[repetition].addressModeV; 15821 } 15822 } 15823 /** 15824 * Sets the transform for the pattern 15825 * @param transform - The transform matrix to apply to the pattern. 15826 * If not provided, the pattern will use the default transform. 15827 */ 15828 setTransform(transform) { 15829 const texture = this.texture; 15830 this.transform.copyFrom(transform); 15831 this.transform.invert(); 15832 this.transform.scale( 15833 1 / texture.frame.width, 15834 1 / texture.frame.height 15835 ); 15836 this._tick++; 15837 } 15838 /** Internal texture used to render the gradient */ 15839 get texture() { 15840 return this._texture; 15841 } 15842 set texture(value) { 15843 if (this._texture === value) 15844 return; 15845 this._texture = value; 15846 this._tick++; 15847 } 15848 /** 15849 * Returns a unique key for this instance. 15850 * This key is used for caching. 15851 * @returns {string} Unique key for the instance 15852 */ 15853 get styleKey() { 15854 return `fill-pattern-${this.uid}-${this._tick}`; 15855 } 15856 /** Destroys the fill pattern, releasing resources. This will also destroy the internal texture. */ 15857 destroy() { 15858 this.texture.destroy(true); 15859 this.texture = null; 15860 } 15861} 15862 15863var parseSvgPath; 15864var hasRequiredParseSvgPath; 15865 15866function requireParseSvgPath () { 15867 if (hasRequiredParseSvgPath) return parseSvgPath; 15868 hasRequiredParseSvgPath = 1; 15869 parseSvgPath = parse; 15870 15871 /** 15872 * expected argument lengths 15873 * @type {Object} 15874 */ 15875 15876 var length = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0}; 15877 15878 /** 15879 * segment pattern 15880 * @type {RegExp} 15881 */ 15882 15883 var segment = /([astvzqmhlc])([^astvzqmhlc]*)/ig; 15884 15885 /** 15886 * parse an svg path data string. Generates an Array 15887 * of commands where each command is an Array of the 15888 * form `[command, arg1, arg2, ...]` 15889 * 15890 * @param {String} path 15891 * @return {Array} 15892 */ 15893 15894 function parse(path) { 15895 var data = []; 15896 path.replace(segment, function(_, command, args){ 15897 var type = command.toLowerCase(); 15898 args = parseValues(args); 15899 15900 // overloaded moveTo 15901 if (type == 'm' && args.length > 2) { 15902 data.push([command].concat(args.splice(0, 2))); 15903 type = 'l'; 15904 command = command == 'm' ? 'l' : 'L'; 15905 } 15906 15907 while (true) { 15908 if (args.length == length[type]) { 15909 args.unshift(command); 15910 return data.push(args) 15911 } 15912 if (args.length < length[type]) throw new Error('malformed path data') 15913 data.push([command].concat(args.splice(0, length[type]))); 15914 } 15915 }); 15916 return data 15917 } 15918 15919 var number = /-?[0-9]*\.?[0-9]+(?:e[-+]?\d+)?/ig; 15920 15921 function parseValues(args) { 15922 var numbers = args.match(number); 15923 return numbers ? numbers.map(Number) : [] 15924 } 15925 return parseSvgPath; 15926} 15927 15928var parseSvgPathExports = requireParseSvgPath(); 15929const parse = /*@__PURE__*/getDefaultExportFromCjs(parseSvgPathExports); 15930 15931function parseSVGPath(svgPath, path) { 15932 const commands = parse(svgPath); 15933 const subpaths = []; 15934 let currentSubPath = null; 15935 let lastX = 0; 15936 let lastY = 0; 15937 for (let i = 0; i < commands.length; i++) { 15938 const command = commands[i]; 15939 const type = command[0]; 15940 const data = command; 15941 switch (type) { 15942 case "M": 15943 lastX = data[1]; 15944 lastY = data[2]; 15945 path.moveTo(lastX, lastY); 15946 break; 15947 case "m": 15948 lastX += data[1]; 15949 lastY += data[2]; 15950 path.moveTo(lastX, lastY); 15951 break; 15952 case "H": 15953 lastX = data[1]; 15954 path.lineTo(lastX, lastY); 15955 break; 15956 case "h": 15957 lastX += data[1]; 15958 path.lineTo(lastX, lastY); 15959 break; 15960 case "V": 15961 lastY = data[1]; 15962 path.lineTo(lastX, lastY); 15963 break; 15964 case "v": 15965 lastY += data[1]; 15966 path.lineTo(lastX, lastY); 15967 break; 15968 case "L": 15969 lastX = data[1]; 15970 lastY = data[2]; 15971 path.lineTo(lastX, lastY); 15972 break; 15973 case "l": 15974 lastX += data[1]; 15975 lastY += data[2]; 15976 path.lineTo(lastX, lastY); 15977 break; 15978 case "C": 15979 lastX = data[5]; 15980 lastY = data[6]; 15981 path.bezierCurveTo( 15982 data[1], 15983 data[2], 15984 // First control point 15985 data[3], 15986 data[4], 15987 // Second control point 15988 lastX, 15989 lastY 15990 // End point 15991 ); 15992 break; 15993 case "c": 15994 path.bezierCurveTo( 15995 lastX + data[1], 15996 lastY + data[2], 15997 // First control point 15998 lastX + data[3], 15999 lastY + data[4], 16000 // Second control point 16001 lastX + data[5], 16002 lastY + data[6] 16003 // End point 16004 ); 16005 lastX += data[5]; 16006 lastY += data[6]; 16007 break; 16008 case "S": 16009 lastX = data[3]; 16010 lastY = data[4]; 16011 path.bezierCurveToShort( 16012 data[1], 16013 data[2], 16014 // Control point 16015 lastX, 16016 lastY 16017 // End point 16018 ); 16019 break; 16020 case "s": 16021 path.bezierCurveToShort( 16022 lastX + data[1], 16023 lastY + data[2], 16024 // Control point 16025 lastX + data[3], 16026 lastY + data[4] 16027 // End point 16028 ); 16029 lastX += data[3]; 16030 lastY += data[4]; 16031 break; 16032 case "Q": 16033 lastX = data[3]; 16034 lastY = data[4]; 16035 path.quadraticCurveTo( 16036 data[1], 16037 data[2], 16038 // Control point 16039 lastX, 16040 lastY 16041 // End point 16042 ); 16043 break; 16044 case "q": 16045 path.quadraticCurveTo( 16046 lastX + data[1], 16047 lastY + data[2], 16048 // Control point 16049 lastX + data[3], 16050 lastY + data[4] 16051 // End point 16052 ); 16053 lastX += data[3]; 16054 lastY += data[4]; 16055 break; 16056 case "T": 16057 lastX = data[1]; 16058 lastY = data[2]; 16059 path.quadraticCurveToShort( 16060 lastX, 16061 lastY 16062 // End point 16063 ); 16064 break; 16065 case "t": 16066 lastX += data[1]; 16067 lastY += data[2]; 16068 path.quadraticCurveToShort( 16069 lastX, 16070 lastY 16071 // End point 16072 ); 16073 break; 16074 case "A": 16075 lastX = data[6]; 16076 lastY = data[7]; 16077 path.arcToSvg( 16078 data[1], 16079 // rx 16080 data[2], 16081 // ry 16082 data[3], 16083 // x-axis-rotation 16084 data[4], 16085 // large-arc-flag 16086 data[5], 16087 // sweep-flag 16088 lastX, 16089 lastY 16090 // End point 16091 ); 16092 break; 16093 case "a": 16094 lastX += data[6]; 16095 lastY += data[7]; 16096 path.arcToSvg( 16097 data[1], 16098 // rx 16099 data[2], 16100 // ry 16101 data[3], 16102 // x-axis-rotation 16103 data[4], 16104 // large-arc-flag 16105 data[5], 16106 // sweep-flag 16107 lastX, 16108 lastY 16109 // End point 16110 ); 16111 break; 16112 case "Z": 16113 case "z":
16114 path.closePath(); 16115 if (subpaths.length > 0) { 16116 currentSubPath = subpaths.pop(); 16117 if (currentSubPath) { 16118 lastX = currentSubPath.startX; 16119 lastY = currentSubPath.startY; 16120 } else { 16121 lastX = 0; 16122 lastY = 0; 16123 } 16124 } 16125 currentSubPath = null; 16126 break; 16127 default: 16128 warn(`Unknown SVG path command: ${type}`); 16129 } 16130 if (type !== "Z" && type !== "z") { 16131 if (currentSubPath === null) { 16132 currentSubPath = { startX: lastX, startY: lastY }; 16133 subpaths.push(currentSubPath); 16134 } 16135 } 16136 } 16137 return path; 16138} 16139 16140class Circle { 16141 /** 16142 * @param x - The X coordinate of the center of this circle 16143 * @param y - The Y coordinate of the center of this circle 16144 * @param radius - The radius of the circle 16145 */ 16146 constructor(x = 0, y = 0, radius = 0) { 16147 /** 16148 * The type of the object, mainly used to avoid `instanceof` checks. 16149 * @example 16150 * ```ts 16151 * // Check shape type 16152 * const shape = new Circle(0, 0, 50); 16153 * console.log(shape.type); // 'circle' 16154 * 16155 * // Use in type guards 16156 * if (shape.type === 'circle') { 16157 * console.log(shape.radius); 16158 * } 16159 * ``` 16160 * @remarks 16161 * - Used for shape type checking 16162 * - More efficient than instanceof 16163 * - Read-only property 16164 * @readonly 16165 * @default 'circle' 16166 * @see {@link SHAPE_PRIMITIVE} For all shape types 16167 * @see {@link ShapePrimitive} For shape interface 16168 */ 16169 this.type = "circle"; 16170 this.x = x; 16171 this.y = y; 16172 this.radius = radius; 16173 } 16174 /** 16175 * Creates a clone of this Circle instance. 16176 * @example 16177 * ```ts 16178 * // Basic circle cloning 16179 * const original = new Circle(100, 100, 50); 16180 * const copy = original.clone(); 16181 * 16182 * // Clone and modify 16183 * const modified = original.clone(); 16184 * modified.radius = 75; 16185 * 16186 * // Verify independence 16187 * console.log(original.radius); // 50 16188 * console.log(modified.radius); // 75 16189 * ``` 16190 * @returns A copy of the Circle 16191 * @see {@link Circle.copyFrom} For copying into existing circle 16192 * @see {@link Circle.copyTo} For copying to another circle 16193 */ 16194 clone() { 16195 return new Circle(this.x, this.y, this.radius); 16196 } 16197 /** 16198 * Checks whether the x and y coordinates given are contained within this circle. 16199 * 16200 * Uses the distance formula to determine if a point is inside the circle's radius. 16201 * 16202 * Commonly used for hit testing in PixiJS events and graphics. 16203 * @example 16204 * ```ts 16205 * // Basic containment check 16206 * const circle = new Circle(100, 100, 50); 16207 * const isInside = circle.contains(120, 120); 16208 * 16209 * // Check mouse position 16210 * const circle = new Circle(0, 0, 100); 16211 * container.hitArea = circle; 16212 * container.on('pointermove', (e) => {
16213 * // only called if pointer is within circle 16214 * }); 16215 * ``` 16216 * @param x - The X coordinate of the point to test 16217 * @param y - The Y coordinate of the point to test 16218 * @returns Whether the x/y coordinates are within this Circle 16219 * @see {@link Circle.strokeContains} For checking stroke intersection 16220 * @see {@link Circle.getBounds} For getting bounding box 16221 */ 16222 contains(x, y) { 16223 if (this.radius <= 0) 16224 return false; 16225 const r2 = this.radius * this.radius; 16226 let dx = this.x - x; 16227 let dy = this.y - y; 16228 dx *= dx; 16229 dy *= dy; 16230 return dx + dy <= r2; 16231 } 16232 /** 16233 * Checks whether the x and y coordinates given are contained within this circle including the stroke. 16234 * @example 16235 * ```ts 16236 * // Basic stroke check 16237 * const circle = new Circle(100, 100, 50); 16238 * const isOnStroke = circle.strokeContains(150, 100, 4); // 4px line width 16239 * 16240 * // Check with different alignments 16241 * const innerStroke = circle.strokeContains(150, 100, 4, 1); // Inside 16242 * const centerStroke = circle.strokeContains(150, 100, 4, 0.5); // Centered 16243 * const outerStroke = circle.strokeContains(150, 100, 4, 0); // Outside 16244 * ``` 16245 * @param x - The X coordinate of the point to test 16246 * @param y - The Y coordinate of the point to test 16247 * @param width - The width of the line to check 16248 * @param alignment - The alignment of the stroke, 0.5 by default 16249 * @returns Whether the x/y coordinates are within this Circle's stroke 16250 * @see {@link Circle.contains} For checking fill containment 16251 * @see {@link Circle.getBounds} For getting stroke bounds 16252 */ 16253 strokeContains(x, y, width, alignment = 0.5) { 16254 if (this.radius === 0) 16255 return false; 16256 const dx = this.x - x; 16257 const dy = this.y - y; 16258 const radius = this.radius; 16259 const outerWidth = (1 - alignment) * width; 16260 const distance = Math.sqrt(dx * dx + dy * dy); 16261 return distance <= radius + outerWidth && distance > radius - (width - outerWidth); 16262 } 16263 /** 16264 * Returns the framing rectangle of the circle as a Rectangle object. 16265 * @example 16266 * ```ts 16267 * // Basic bounds calculation 16268 * const circle = new Circle(100, 100, 50); 16269 * const bounds = circle.getBounds(); 16270 * // bounds: x=50, y=50, width=100, height=100 16271 * 16272 * // Reuse existing rectangle 16273 * const rect = new Rectangle(); 16274 * circle.getBounds(rect); 16275 * ``` 16276 * @param out - Optional Rectangle object to store the result 16277 * @returns The framing rectangle 16278 * @see {@link Rectangle} For rectangle properties 16279 * @see {@link Circle.contains} For point containment 16280 */ 16281 getBounds(out) { 16282 out || (out = new Rectangle()); 16283 out.x = this.x - this.radius; 16284 out.y = this.y - this.radius; 16285 out.width = this.radius * 2; 16286 out.height = this.radius * 2; 16287 return out; 16288 } 16289 /** 16290 * Copies another circle to this one. 16291 * @example 16292 * ```ts 16293 * // Basic copying 16294 * const source = new Circle(100, 100, 50); 16295 * const target = new Circle(); 16296 * target.copyFrom(source); 16297 * ``` 16298 * @param circle - The circle to copy from 16299 * @returns Returns itself 16300 * @see {@link Circle.copyTo} For copying to another circle 16301 * @see {@link Circle.clone} For creating new circle copy 16302 */ 16303 copyFrom(circle) {
vendor: 4,401 bytes, lines 16304-16436
16304 this.x = circle.x; 16305 this.y = circle.y; 16306 this.radius = circle.radius; 16307 return this; 16308 } 16309 /** 16310 * Copies this circle to another one. 16311 * @example 16312 * ```ts 16313 * // Basic copying 16314 * const source = new Circle(100, 100, 50); 16315 * const target = new Circle(); 16316 * source.copyTo(target); 16317 * ``` 16318 * @param circle - The circle to copy to 16319 * @returns Returns given parameter 16320 * @see {@link Circle.copyFrom} For copying from another circle 16321 * @see {@link Circle.clone} For creating new circle copy 16322 */ 16323 copyTo(circle) { 16324 circle.copyFrom(this); 16325 return circle; 16326 } 16327 toString() { 16328 return `[pixi.js/math:Circle x=${this.x} y=${this.y} radius=${this.radius}]`; 16329 } 16330} 16331 16332class Ellipse { 16333 /** 16334 * @param x - The X coordinate of the center of this ellipse 16335 * @param y - The Y coordinate of the center of this ellipse 16336 * @param halfWidth - The half width of this ellipse 16337 * @param halfHeight - The half height of this ellipse 16338 */ 16339 constructor(x = 0, y = 0, halfWidth = 0, halfHeight = 0) { 16340 /** 16341 * The type of the object, mainly used to avoid `instanceof` checks 16342 * @example 16343 * ```ts 16344 * // Check shape type 16345 * const shape = new Ellipse(0, 0, 50, 25); 16346 * console.log(shape.type); // 'ellipse' 16347 * 16348 * // Use in type guards 16349 * if (shape.type === 'ellipse') { 16350 * console.log(shape.halfWidth, shape.halfHeight); 16351 * } 16352 * ``` 16353 * @readonly 16354 * @default 'ellipse' 16355 * @see {@link SHAPE_PRIMITIVE} For all shape types 16356 */ 16357 this.type = "ellipse"; 16358 this.x = x; 16359 this.y = y; 16360 this.halfWidth = halfWidth; 16361 this.halfHeight = halfHeight; 16362 } 16363 /** 16364 * Creates a clone of this Ellipse instance. 16365 * @example 16366 * ```ts 16367 * // Basic cloning 16368 * const original = new Ellipse(100, 100, 50, 25); 16369 * const copy = original.clone(); 16370 * 16371 * // Clone and modify 16372 * const modified = original.clone(); 16373 * modified.halfWidth *= 2; 16374 * modified.halfHeight *= 2; 16375 * 16376 * // Verify independence 16377 * console.log(original.halfWidth); // 50 16378 * console.log(modified.halfWidth); // 100 16379 * ``` 16380 * @returns A copy of the ellipse 16381 * @see {@link Ellipse.copyFrom} For copying into existing ellipse 16382 * @see {@link Ellipse.copyTo} For copying to another ellipse 16383 */ 16384 clone() { 16385 return new Ellipse(this.x, this.y, this.halfWidth, this.halfHeight); 16386 } 16387 /** 16388 * Checks whether the x and y coordinates given are contained within this ellipse. 16389 * Uses normalized coordinates and the ellipse equation to determine containment. 16390 * @example 16391 * ```ts 16392 * // Basic containment check 16393 * const ellipse = new Ellipse(100, 100, 50, 25); 16394 * const isInside = ellipse.contains(120, 110); 16395 * ``` 16396 * @remarks 16397 * - Uses ellipse equation (x²/a² + y²/b² ≤ 1) 16398 * - Returns false if dimensions are 0 or negative 16399 * - Normalized to center (0,0) for calculation 16400 * @param x - The X coordinate of the point to test 16401 * @param y - The Y coordinate of the point to test 16402 * @returns Whether the x/y coords are within this ellipse 16403 * @see {@link Ellipse.strokeContains} For checking stroke intersection 16404 * @see {@link Ellipse.getBounds} For getting containing rectangle 16405 */ 16406 contains(x, y) { 16407 if (this.halfWidth <= 0 || this.halfHeight <= 0) { 16408 return false; 16409 } 16410 let normx = (x - this.x) / this.halfWidth; 16411 let normy = (y - this.y) / this.halfHeight; 16412 normx *= normx; 16413 normy *= normy; 16414 return normx + normy <= 1; 16415 } 16416 /** 16417 * Checks whether the x and y coordinates given are contained within this ellipse including stroke. 16418 * @example 16419 * ```ts 16420 * // Basic stroke check 16421 * const ellipse = new Ellipse(100, 100, 50, 25); 16422 * const isOnStroke = ellipse.strokeContains(150, 100, 4); // 4px line width 16423 * 16424 * // Check with different alignments 16425 * const innerStroke = ellipse.strokeContains(150, 100, 4, 1); // Inside 16426 * const centerStroke = ellipse.strokeContains(150, 100, 4, 0.5); // Centered 16427 * const outerStroke = ellipse.strokeContains(150, 100, 4, 0); // Outside 16428 * ``` 16429 * @remarks 16430 * - Uses normalized ellipse equations 16431 * - Considers stroke alignment 16432 * - Returns false if dimensions are 0 16433 * @param x - The X coordinate of the point to test 16434 * @param y - The Y coordinate of the point to test 16435 * @param strokeWidth - The width of the line to check 16436 * @param alignment - The alignment of the stroke (1 = inner,
164360.5 = centered, 0 = outer) 16437 * @returns Whether the x/y coords are within this ellipse's stroke 16438 * @see {@link Ellipse.contains} For checking fill containment 16439 * @see {@link Ellipse.getBounds} For getting stroke bounds 16440 */ 16441 strokeContains(x, y, strokeWidth, alignment = 0.5) { 16442 const { halfWidth, halfHeight } = this; 16443 if (halfWidth <= 0 || halfHeight <= 0) { 16444 return false; 16445 } 16446 const strokeOuterWidth = strokeWidth * (1 - alignment); 16447 const strokeInnerWidth = strokeWidth - strokeOuterWidth; 16448 const innerHorizontal = halfWidth - strokeInnerWidth; 16449 const innerVertical = halfHeight - strokeInnerWidth; 16450 const outerHorizontal = halfWidth + strokeOuterWidth; 16451 const outerVertical = halfHeight + strokeOuterWidth; 16452 const normalizedX = x - this.x; 16453 const normalizedY = y - this.y; 16454 const innerEllipse = normalizedX * normalizedX / (innerHorizontal * innerHorizontal) + normalizedY * normalizedY / (innerVertical * innerVertical); 16455 const outerEllipse = normalizedX * normalizedX / (outerHorizontal * outerHorizontal) + normalizedY * normalizedY / (outerVertical * outerVertical); 16456 return innerEllipse > 1 && outerEllipse <= 1; 16457 } 16458 /** 16459 * Returns the framing rectangle of the ellipse as a Rectangle object. 16460 * @example 16461 * ```ts 16462 * // Basic bounds calculation 16463 * const ellipse = new Ellipse(100, 100, 50, 25); 16464 * const bounds = ellipse.getBounds(); 16465 * // bounds: x=50, y=75, width=100, height=50 16466 * 16467 * // Reuse existing rectangle 16468 * const rect = new Rectangle(); 16469 * ellipse.getBounds(rect); 16470 * ``` 16471 * @remarks 16472 * - Creates Rectangle if none provided 16473 * - Top-left is (x-halfWidth, y-halfHeight) 16474 * - Width is halfWidth * 2 16475 * - Height is halfHeight * 2 16476 * @param out - Optional Rectangle object to store the result 16477 * @returns The framing rectangle 16478 * @see {@link Rectangle} For rectangle properties 16479 * @see {@link Ellipse.contains} For checking if a point is inside 16480 */ 16481 getBounds(out) { 16482 out || (out = new Rectangle()); 16483 out.x = this.x - this.halfWidth; 16484 out.y = this.y - this.halfHeight; 16485 out.width = this.halfWidth * 2; 16486 out.height = this.halfHeight * 2; 16487 return out; 16488 } 16489 /** 16490 * Copies another ellipse to this one. 16491 * @example 16492 * ```ts 16493 * // Basic copying 16494 * const source = new Ellipse(100, 100, 50, 25); 16495 * const target = new Ellipse(); 16496 * target.copyFrom(source); 16497 * ``` 16498 * @param ellipse - The ellipse to copy from 16499 * @returns Returns itself 16500 * @see {@link Ellipse.copyTo} For copying to another ellipse 16501 * @see {@link Ellipse.clone} For creating new ellipse copy 16502 */ 16503 copyFrom(ellipse) { 16504 this.x = ellipse.x; 16505 this.y = ellipse.y; 16506 this.halfWidth = ellipse.halfWidth; 16507 this.halfHeight = ellipse.halfHeight; 16508 return this; 16509 } 16510 /** 16511 * Copies this ellipse to another one. 16512 * @example 16513 * ```ts 16514 * // Basic copying 16515 * const source = new Ellipse(100, 100, 50, 25); 16516 * const target = new Ellipse(); 16517 * source.copyTo(target); 16518 * ``` 16519 * @param ellipse - The ellipse to copy to 16520 * @returns Returns given parameter 16521 * @see {@link Ellipse.copyFrom} For copying from another ellipse 16522 * @see {@link Ellipse.clone} For creating new ellipse copy 16523 */ 16524 copyTo(ellipse) { 16525 ellipse.copyFrom(this); 16526 return ellipse; 16527 } 16528 toString() { 16529 return `[pixi.js/math:Ellipse x=${this.x} y=${this.y} halfWidth=${this.halfWidth} halfHeight=${this.halfHeight}]`; 16530 } 16531} 16532 16533function squaredDistanceToLineSegment(x, y, x1, y1, x2, y2) { 16534 const a = x - x1; 16535 const b = y - y1; 16536 const c = x2 - x1; 16537 const d = y2 - y1; 16538 const dot = a * c + b * d; 16539 const lenSq = c * c + d * d; 16540 let param = -1; 16541 if (lenSq !== 0) { 16542 param = dot / lenSq; 16543 } 16544 let xx; 16545 let yy; 16546 if (param < 0) { 16547 xx = x1; 16548 yy = y1; 16549 } else if (param > 1) { 16550 xx = x2; 16551 yy = y2; 16552 } else { 16553 xx = x1 + param * c; 16554 yy = y1 + param * d; 16555 } 16556 const dx = x - xx; 16557 const dy = y - yy; 16558 return dx * dx + dy * dy; 16559} 16560 16561let tempRect$2; 16562let tempRect2; 16563class Polygon { 16564 /** 16565 * @param points - This can be an array of Points 16566 * that form the polygon, a flat array of numbers that will be interpreted as [x,y, x,y, ...], or 16567 * the arguments passed can be all the points of the polygon e.g. 16568 * `new Polygon(new Point(), new Point(), ...)`, or the arguments passed can be flat 16569 * x,y values e.g. `new Polygon(x,y, x,y, x,y, ...)` where `x` and `y` are Numbers. 16570 */ 16571 constructor(...points) { 16572 /** 16573 * The type of the object, mainly used to avoid `instanceof` checks 16574 * @example 16575 * ```ts
16576 * // Check shape type 16577 * const shape = new Polygon([0, 0, 100, 0, 50, 100]); 16578 * console.log(shape.type); // 'polygon' 16579 * 16580 * // Use in type guards 16581 * if (shape.type === 'polygon') { 16582 * // TypeScript knows this is a Polygon 16583 * console.log(shape.points.length); 16584 * } 16585 * ``` 16586 * @readonly 16587 * @default 'polygon' 16588 * @see {@link SHAPE_PRIMITIVE} For all shape types 16589 */ 16590 this.type = "polygon"; 16591 let flat = Array.isArray(points[0]) ? points[0] : points; 16592 if (typeof flat[0] !== "number") { 16593 const p = []; 16594 for (let i = 0, il = flat.length; i < il; i++) { 16595 p.push(flat[i].x, flat[i].y); 16596 } 16597 flat = p; 16598 } 16599 this.points = flat; 16600 this.closePath = true; 16601 } 16602 /** 16603 * Determines whether the polygon's points are arranged in a clockwise direction. 16604 * Uses the shoelace formula (surveyor's formula) to calculate the signed area. 16605 * 16606 * A positive area indicates clockwise winding, while negative indicates counter-clockwise. 16607 * 16608 * The formula sums up the cross products of adjacent vertices: 16609 * For each pair of adjacent points (x1,y1) and (x2,y2), we calculate (x1*y2 - x2*y1) 16610 * The final sum divided by 2 gives the signed area - positive for clockwise. 16611 * @example 16612 * ```ts 16613 * // Check polygon winding 16614 * const polygon = new Polygon([0, 0, 100, 0, 50, 100]); 16615 * console.log(polygon.isClockwise()); // Check direction 16616 * 16617 * // Use in path construction 16618 * const hole = new Polygon([25, 25, 75, 25, 75, 75, 25, 75]); 16619 * if (hole.isClockwise() === shape.isClockwise()) { 16620 * hole.points.reverse(); // Reverse for proper hole winding 16621 * } 16622 * ``` 16623 * @returns `true` if the polygon's points are arranged clockwise, `false` if counter-clockwise 16624 */ 16625 isClockwise() { 16626 let area = 0; 16627 const points = this.points; 16628 const length = points.length; 16629 for (let i = 0; i < length; i += 2) { 16630 const x1 = points[i]; 16631 const y1 = points[i + 1]; 16632 const x2 = points[(i + 2) % length]; 16633 const y2 = points[(i + 3) % length]; 16634 area += (x2 - x1) * (y2 + y1); 16635 } 16636 return area < 0; 16637 } 16638 /** 16639 * Checks if this polygon completely contains another polygon. 16640 * Used for detecting holes in shapes, like when parsing SVG paths. 16641 * @example 16642 * ```ts 16643 * // Basic containment check 16644 * const outerSquare = new Polygon([0,0, 100,0, 100,100, 0,100]); // A square 16645 * const innerSquare = new Polygon([25,25, 75,25, 75,75, 25,75]); // A smaller square inside 16646 * 16647 * outerSquare.containsPolygon(innerSquare); // Returns true 16648 * innerSquare.containsPolygon(outerSquare); // Returns false 16649 * ``` 16650 * @remarks 16651 * - Uses bounds check for quick rejection 16652 * - Tests all points for containment 16653 * @param polygon - The polygon to test for containment 16654 * @returns True if this polygon completely contains the other polygon 16655 * @see {@link Polygon.contains} For single point testing 16656 * @see {@link Polygon.getBounds} For bounds calculation 16657 */ 16658 containsPolygon(polygon) { 16659 const thisBounds = this.getBounds(tempRect$2); 16660 const otherBounds = polygon.getBounds(tempRect2); 16661 if (!thisBounds.containsRect(otherBounds)) { 16662 return false; 16663 } 16664 const points = polygon.points; 16665 for (let i = 0; i < points.length; i += 2) { 16666 const x = points[i]; 16667 const y = points[i + 1]; 16668 if (!this.contains(x, y)) { 16669 return false; 16670 } 16671 } 16672 return true; 16673 } 16674 /** 16675 * Creates a clone of this polygon. 16676 * @example 16677 * ```ts 16678 * // Basic cloning 16679 * const original = new Polygon([0, 0, 100, 0, 50, 100]); 16680 * const copy = original.clone(); 16681 * 16682 * // Clone and modify 16683 * const modified = original.clone(); 16684 * modified.points[0] = 10; // Modify first x coordinate 16685 * ``` 16686 * @returns A copy of the polygon 16687 * @see {@link Polygon.copyFrom} For copying into existing polygon 16688 * @see {@link Polygon.copyTo} For copying to another polygon 16689 */ 16690 clone() { 16691 const points = this.points.slice(); 16692 const polygon = new Polygon(points); 16693 polygon.closePath = this.closePath; 16694 return polygon; 16695 } 16696 /** 16697 * Checks whether the x and y coordinates passed to this function are contained within this polygon. 16698 * Uses raycasting algorithm for point-in-polygon testing. 16699 * @example 16700 * ```ts 16701 * // Basic containment check 16702 * const polygon = new Polygon([0, 0, 100, 0, 50, 100]); 16703 * const isInside = polygon.contains(25, 25); // true 16704 * ``` 16705 * @param x - The X coordinate of the point to test 16706 * @param y - The Y coordinate of the point to test 16707 * @returns Whether the x/y coordinates are within this polygon 16708 * @see {@link Polygon.strokeContains} For checking stroke intersection 16709 * @see {@link Polygon.containsPolygon} For polygon-in-polygon testing 16710 */ 16711 contains(x, y) { 16712 let inside = false;
vendor: 5,087 bytes, lines 16713-16855
16713 const length = this.points.length / 2; 16714 for (let i = 0, j = length - 1; i < length; j = i++) { 16715 const xi = this.points[i * 2]; 16716 const yi = this.points[i * 2 + 1]; 16717 const xj = this.points[j * 2]; 16718 const yj = this.points[j * 2 + 1]; 16719 const intersect = yi > y !== yj > y && x < (xj - xi) * ((y - yi) / (yj - yi)) + xi; 16720 if (intersect) { 16721 inside = !inside; 16722 } 16723 } 16724 return inside; 16725 } 16726 /** 16727 * Checks whether the x and y coordinates given are contained within this polygon including the stroke. 16728 * @example 16729 * ```ts 16730 * // Basic stroke check 16731 * const polygon = new Polygon([0, 0, 100, 0, 50, 100]); 16732 * const isOnStroke = polygon.strokeContains(25, 25, 4); // 4px line width 16733 * 16734 * // Check with different alignments 16735 * const innerStroke = polygon.strokeContains(25, 25, 4, 1); // Inside 16736 * const centerStroke = polygon.strokeContains(25, 25, 4, 0.5); // Centered 16737 * const outerStroke = polygon.strokeContains(25, 25, 4, 0); // Outside 16738 * ``` 16739 * @param x - The X coordinate of the point to test 16740 * @param y - The Y coordinate of the point to test 16741 * @param strokeWidth - The width of the line to check 16742 * @param alignment - The alignment of the stroke (1 = inner, 0.5 = centered, 0 = outer) 16743 * @returns Whether the x/y coordinates are within this polygon's stroke 16744 * @see {@link Polygon.contains} For checking fill containment 16745 * @see {@link Polygon.getBounds} For getting stroke bounds 16746 */ 16747 strokeContains(x, y, strokeWidth, alignment = 0.5) { 16748 const strokeWidthSquared = strokeWidth * strokeWidth; 16749 const rightWidthSquared = strokeWidthSquared * (1 - alignment); 16750 const leftWidthSquared = strokeWidthSquared - rightWidthSquared; 16751 const { points } = this; 16752 const iterationLength = points.length - (this.closePath ? 0 : 2); 16753 for (let i = 0; i < iterationLength; i += 2) { 16754 const x1 = points[i]; 16755 const y1 = points[i + 1]; 16756 const x2 = points[(i + 2) % points.length]; 16757 const y2 = points[(i + 3) % points.length]; 16758 const distanceSquared = squaredDistanceToLineSegment(x, y, x1, y1, x2, y2); 16759 const sign = Math.sign((x2 - x1) * (y - y1) - (y2 - y1) * (x - x1)); 16760 if (distanceSquared <= (sign < 0 ? leftWidthSquared : rightWidthSquared)) { 16761 return true; 16762 } 16763 } 16764 return false; 16765 } 16766 /** 16767 * Returns the framing rectangle of the polygon as a Rectangle object. 16768 * @example 16769 * ```ts 16770 * // Basic bounds calculation 16771 * const polygon = new Polygon([0, 0, 100, 0, 50, 100]); 16772 * const bounds = polygon.getBounds(); 16773 * // bounds: x=0, y=0, width=100, height=100 16774 * 16775 * // Reuse existing rectangle 16776 * const rect = new Rectangle(); 16777 * polygon.getBounds(rect); 16778 * ``` 16779 * @param out - Optional rectangle to store the result 16780 * @returns The framing rectangle 16781 * @see {@link Rectangle} For rectangle properties 16782 * @see {@link Polygon.contains} For checking if a point is inside 16783 */ 16784 getBounds(out) { 16785 out || (out = new Rectangle()); 16786 const points = this.points; 16787 let minX = Infinity; 16788 let maxX = -Infinity; 16789 let minY = Infinity; 16790 let maxY = -Infinity; 16791 for (let i = 0, n = points.length; i < n; i += 2) { 16792 const x = points[i]; 16793 const y = points[i + 1]; 16794 minX = x < minX ? x : minX; 16795 maxX = x > maxX ? x : maxX; 16796 minY = y < minY ? y : minY; 16797 maxY = y > maxY ? y : maxY; 16798 } 16799 out.x = minX; 16800 out.width = maxX - minX; 16801 out.y = minY; 16802 out.height = maxY - minY; 16803 return out; 16804 } 16805 /** 16806 * Copies another polygon to this one. 16807 * @example 16808 * ```ts 16809 * // Basic copying 16810 * const source = new Polygon([0, 0, 100, 0, 50, 100]); 16811 * const target = new Polygon(); 16812 * target.copyFrom(source); 16813 * ``` 16814 * @param polygon - The polygon to copy from 16815 * @returns Returns itself 16816 * @see {@link Polygon.copyTo} For copying to another polygon 16817 * @see {@link Polygon.clone} For creating new polygon copy 16818 */ 16819 copyFrom(polygon) { 16820 this.points = polygon.points.slice(); 16821 this.closePath = polygon.closePath; 16822 return this; 16823 } 16824 /** 16825 * Copies this polygon to another one. 16826 * @example 16827 * ```ts 16828 * // Basic copying 16829 * const source = new Polygon([0, 0, 100, 0, 50, 100]); 16830 * const target = new Polygon(); 16831 * source.copyTo(target); 16832 * ``` 16833 * @param polygon - The polygon to copy to 16834 * @returns Returns given parameter 16835 * @see {@link Polygon.copyFrom} For copying from another polygon 16836 * @see {@link Polygon.clone} For creating new polygon copy 16837 */ 16838 copyTo(polygon) { 16839 polygon.copyFrom(this); 16840 return polygon; 16841 } 16842 toString() { 16843 return `[pixi.js/math:PolygoncloseStroke=${this.closePath}points=${this.points.reduce((pointsDesc, currentPoint) => `${pointsDesc}, ${currentPoint}`, "")}]`; 16844 } 16845 /** 16846 * Get the last X coordinate of the polygon. 16847 * @example 16848 * ```ts 16849 * // Basic coordinate access 16850 * const polygon = new Polygon([0, 0, 100, 200, 300, 400]); 16851 * console.log(polygon.lastX); // 300 16852 * ``` 16853 * @readonly 16854 * @returns The x-coordinate of the last vertex 16855 * @see {@link Polygon.lastY}
16855 For last Y coordinate 16856 * @see {@link Polygon.points} For raw points array 16857 */ 16858 get lastX() { 16859 return this.points[this.points.length - 2]; 16860 } 16861 /** 16862 * Get the last Y coordinate of the polygon. 16863 * @example 16864 * ```ts 16865 * // Basic coordinate access 16866 * const polygon = new Polygon([0, 0, 100, 200, 300, 400]); 16867 * console.log(polygon.lastY); // 400 16868 * ``` 16869 * @readonly 16870 * @returns The y-coordinate of the last vertex 16871 * @see {@link Polygon.lastX} For last X coordinate 16872 * @see {@link Polygon.points} For raw points array 16873 */ 16874 get lastY() { 16875 return this.points[this.points.length - 1]; 16876 } 16877 /** 16878 * Get the last X coordinate of the polygon. 16879 * @readonly 16880 * @deprecated since 8.11.0, use {@link Polygon.lastX} instead. 16881 */ 16882 get x() { 16883 deprecation("8.11.0", "Polygon.lastX is deprecated, please use Polygon.lastX instead."); 16884 return this.points[this.points.length - 2]; 16885 } 16886 /** 16887 * Get the last Y coordinate of the polygon. 16888 * @readonly 16889 * @deprecated since 8.11.0, use {@link Polygon.lastY} instead. 16890 */ 16891 get y() { 16892 deprecation("8.11.0", "Polygon.y is deprecated, please use Polygon.lastY instead."); 16893 return this.points[this.points.length - 1]; 16894 } 16895 /** 16896 * Get the first X coordinate of the polygon. 16897 * @example 16898 * ```ts 16899 * // Basic coordinate access 16900 * const polygon = new Polygon([0, 0, 100, 200, 300, 400]); 16901 * console.log(polygon.x); // 0 16902 * ``` 16903 * @readonly 16904 * @returns The x-coordinate of the first vertex 16905 * @see {@link Polygon.startY} For first Y coordinate 16906 * @see {@link Polygon.points} For raw points array 16907 */ 16908 get startX() { 16909 return this.points[0]; 16910 } 16911 /** 16912 * Get the first Y coordinate of the polygon. 16913 * @example 16914 * ```ts 16915 * // Basic coordinate access 16916 * const polygon = new Polygon([0, 0, 100, 200, 300, 400]); 16917 * console.log(polygon.y); // 0 16918 * ``` 16919 * @readonly 16920 * @returns The y-coordinate of the first vertex 16921 * @see {@link Polygon.startX} For first X coordinate 16922 * @see {@link Polygon.points} For raw points array 16923 */ 16924 get startY() { 16925 return this.points[1]; 16926 } 16927} 16928 16929const isCornerWithinStroke = (pX, pY, cornerX, cornerY, radius, strokeWidthInner, strokeWidthOuter) => { 16930 const dx = pX - cornerX; 16931 const dy = pY - cornerY; 16932 const distance = Math.sqrt(dx * dx + dy * dy); 16933 return distance >= radius - strokeWidthInner && distance <= radius + strokeWidthOuter; 16934}; 16935class RoundedRectangle { 16936 /** 16937 * @param x - The X coordinate of the upper-left corner of the rounded rectangle 16938 * @param y - The Y coordinate of the upper-left corner of the rounded rectangle 16939 * @param width - The overall width of this rounded rectangle 16940 * @param height - The overall height of this rounded rectangle 16941 * @param radius - Controls the radius of the rounded corners 16942 */ 16943 constructor(x = 0, y = 0, width = 0, height = 0, radius = 20) { 16944 /** 16945 * The type of the object, mainly used to avoid `instanceof` checks 16946 * @example 16947 * ```ts 16948 * // Check shape type 16949 * const shape = new RoundedRectangle(0, 0, 100, 100, 20); 16950 * console.log(shape.type); // 'roundedRectangle' 16951 * 16952 * // Use in type guards 16953 * if (shape.type === 'roundedRectangle') { 16954 * console.log(shape.radius); 16955 * } 16956 * ``` 16957 * @readonly 16958 * @default 'roundedRectangle' 16959 * @see {@link SHAPE_PRIMITIVE} For all shape types 16960 */ 16961 this.type = "roundedRectangle"; 16962 this.x = x; 16963 this.y = y; 16964 this.width = width; 16965 this.height = height; 16966 this.radius = radius; 16967 } 16968 /** 16969 * Returns the framing rectangle of the rounded rectangle as a Rectangle object 16970 * @example 16971 * ```ts 16972 * // Basic bounds calculation 16973 * const rect = new RoundedRectangle(100, 100, 200, 150, 20); 16974 * const bounds = rect.getBounds(); 16975 * // bounds: x=100, y=100, width=200, height=150 16976 * 16977 * // Reuse existing rectangle 16978 * const out = new Rectangle(); 16979 * rect.getBounds(out); 16980 * ``` 16981 * @remarks 16982 * - Rectangle matches outer dimensions 16983 * - Ignores corner radius 16984 * @param out - Optional rectangle to store the result 16985 * @returns The framing rectangle 16986 * @see {@link Rectangle} For rectangle properties 16987 * @see {@link RoundedRectangle.contains} For checking if a point is inside 16988 */ 16989 getBounds(out) { 16990 out || (out = new Rectangle()); 16991 out.x = this.x; 16992 out.y = this.y; 16993 out.width = this.width; 16994 out.height = this.height; 16995 return out; 16996 } 16997 /** 16998 * Creates a clone of this Rounded Rectangle. 16999 * @example 17000 * ```ts 17001 * // Basic cloning 17002 * const original = new RoundedRectangle(100, 100, 200, 150, 20); 17003 * const copy = original.clone(); 17004 * 17005 * // Clone and modify 17006 * const modified = original.clone(); 17007 * modified.radius = 30; 17008 * modified.width *= 2; 17009 * 17010 * // Verify independence
17011 * console.log(original.radius); // 20 17012 * console.log(modified.radius); // 30 17013 * ``` 17014 * @returns A copy of the rounded rectangle 17015 * @see {@link RoundedRectangle.copyFrom} For copying into existing rectangle 17016 * @see {@link RoundedRectangle.copyTo} For copying to another rectangle 17017 */ 17018 clone() { 17019 return new RoundedRectangle(this.x, this.y, this.width, this.height, this.radius); 17020 } 17021 /** 17022 * Copies another rectangle to this one. 17023 * @example 17024 * ```ts 17025 * // Basic copying 17026 * const source = new RoundedRectangle(100, 100, 200, 150, 20); 17027 * const target = new RoundedRectangle(); 17028 * target.copyFrom(source); 17029 * 17030 * // Chain with other operations 17031 * const rect = new RoundedRectangle() 17032 * .copyFrom(source) 17033 * .getBounds(rect); 17034 * ``` 17035 * @param rectangle - The rectangle to copy from 17036 * @returns Returns itself 17037 * @see {@link RoundedRectangle.copyTo} For copying to another rectangle 17038 * @see {@link RoundedRectangle.clone} For creating new rectangle copy 17039 */ 17040 copyFrom(rectangle) { 17041 this.x = rectangle.x; 17042 this.y = rectangle.y; 17043 this.width = rectangle.width; 17044 this.height = rectangle.height; 17045 return this; 17046 } 17047 /** 17048 * Copies this rectangle to another one. 17049 * @example 17050 * ```ts 17051 * // Basic copying 17052 * const source = new RoundedRectangle(100, 100, 200, 150, 20); 17053 * const target = new RoundedRectangle(); 17054 * source.copyTo(target); 17055 * 17056 * // Chain with other operations 17057 * const result = source 17058 * .copyTo(new RoundedRectangle()) 17059 * .getBounds(); 17060 * ``` 17061 * @param rectangle - The rectangle to copy to 17062 * @returns Returns given parameter 17063 * @see {@link RoundedRectangle.copyFrom} For copying from another rectangle 17064 * @see {@link RoundedRectangle.clone} For creating new rectangle copy 17065 */ 17066 copyTo(rectangle) { 17067 rectangle.copyFrom(this); 17068 return rectangle; 17069 } 17070 /** 17071 * Checks whether the x and y coordinates given are contained within this Rounded Rectangle 17072 * @example 17073 * ```ts 17074 * // Basic containment check 17075 * const rect = new RoundedRectangle(100, 100, 200, 150, 20); 17076 * const isInside = rect.contains(150, 125); // true 17077 * // Check corner radius 17078 * const corner = rect.contains(100, 100); // false if within corner curve 17079 * ``` 17080 * @remarks 17081 * - Returns false if width/height is 0 or negative 17082 * - Handles rounded corners with radius check 17083 * @param x - The X coordinate of the point to test 17084 * @param y - The Y coordinate of the point to test 17085 * @returns Whether the x/y coordinates are within this Rounded Rectangle 17086 * @see {@link RoundedRectangle.strokeContains} For checking stroke intersection 17087 * @see {@link RoundedRectangle.getBounds} For getting containing rectangle 17088 */ 17089 contains(x, y) { 17090 if (this.width <= 0 || this.height <= 0) { 17091 return false; 17092 } 17093 if (x >= this.x && x <= this.x + this.width) { 17094 if (y >= this.y && y <= this.y + this.height) { 17095 const radius = Math.max(0, Math.min(this.radius, Math.min(this.width, this.height) / 2)); 17096 if (y >= this.y + radius && y <= this.y + this.height - radius || x >= this.x + radius && x <= this.x + this.width - radius) { 17097 return true; 17098 } 17099 let dx = x - (this.x + radius); 17100 let dy = y - (this.y + radius); 17101 const radius2 = radius * radius; 17102 if (dx * dx + dy * dy <= radius2) { 17103 return true; 17104 } 17105 dx = x - (this.x + this.width - radius); 17106 if (dx * dx + dy * dy <= radius2) { 17107 return true; 17108 } 17109 dy = y - (this.y + this.height - radius); 17110 if (dx * dx + dy * dy <= radius2) { 17111 return true; 17112 } 17113 dx = x - (this.x + radius); 17114 if (dx * dx + dy * dy <= radius2) { 17115 return true; 17116 } 17117 } 17118 } 17119 return false; 17120 } 17121 /** 17122 * Checks whether the x and y coordinates given are contained within this rectangle including the stroke. 17123 * @example 17124 * ```ts 17125 * // Basic stroke check 17126 * const rect = new RoundedRectangle(100, 100, 200, 150, 20); 17127 * const isOnStroke = rect.strokeContains(150, 100, 4); // 4px line width 17128 * 17129 * // Check with different alignments 17130 * const innerStroke = rect.strokeContains(150, 100, 4, 1); // Inside 17131 * const centerStroke = rect.strokeContains(150, 100, 4, 0.5); // Centered 17132 * const outerStroke = rect.strokeContains(150, 100, 4, 0); // Outside 17133 * ``` 17134 * @param pX - The X coordinate of the point to test 17135 * @param pY - The Y coordinate of the point to test 17136 * @param strokeWidth - The width of the line to check 17137 * @param alignment - The alignment of the stroke (1 = inner,
171370.5 = centered, 0 = outer) 17138 * @returns Whether the x/y coordinates are within this rectangle's stroke 17139 * @see {@link RoundedRectangle.contains} For checking fill containment 17140 * @see {@link RoundedRectangle.getBounds} For getting stroke bounds 17141 */ 17142 strokeContains(pX, pY, strokeWidth, alignment = 0.5) { 17143 const { x, y, width, height, radius } = this; 17144 const strokeWidthOuter = strokeWidth * (1 - alignment); 17145 const strokeWidthInner = strokeWidth - strokeWidthOuter; 17146 const innerX = x + radius; 17147 const innerY = y + radius; 17148 const innerWidth = width - radius * 2; 17149 const innerHeight = height - radius * 2; 17150 const rightBound = x + width; 17151 const bottomBound = y + height; 17152 if ((pX >= x - strokeWidthOuter && pX <= x + strokeWidthInner || pX >= rightBound - strokeWidthInner && pX <= rightBound + strokeWidthOuter) && pY >= innerY && pY <= innerY + innerHeight) { 17153 return true; 17154 } 17155 if ((pY >= y - strokeWidthOuter && pY <= y + strokeWidthInner || pY >= bottomBound - strokeWidthInner && pY <= bottomBound + strokeWidthOuter) && pX >= innerX && pX <= innerX + innerWidth) { 17156 return true; 17157 } 17158 return ( 17159 // Top-left 17160 pX < innerX && pY < innerY && isCornerWithinStroke( 17161 pX, 17162 pY, 17163 innerX, 17164 innerY, 17165 radius, 17166 strokeWidthInner, 17167 strokeWidthOuter 17168 ) || pX > rightBound - radius && pY < innerY && isCornerWithinStroke( 17169 pX, 17170 pY, 17171 rightBound - radius, 17172 innerY, 17173 radius, 17174 strokeWidthInner, 17175 strokeWidthOuter 17176 ) || pX > rightBound - radius && pY > bottomBound - radius && isCornerWithinStroke( 17177 pX, 17178 pY, 17179 rightBound - radius, 17180 bottomBound - radius, 17181 radius, 17182 strokeWidthInner, 17183 strokeWidthOuter 17184 ) || pX < innerX && pY > bottomBound - radius && isCornerWithinStroke( 17185 pX, 17186 pY, 17187 innerX, 17188 bottomBound - radius, 17189 radius, 17190 strokeWidthInner, 17191 strokeWidthOuter 17192 ) 17193 ); 17194 } 17195 toString() { 17196 return `[pixi.js/math:RoundedRectangle x=${this.x} y=${this.y}width=${this.width} height=${this.height} radius=${this.radius}]`; 17197 } 17198} 17199 17200const cachedGroups = {}; 17201function getTextureBatchBindGroup(textures, size, maxTextures) { 17202 let uid = 2166136261; 17203 for (let i = 0; i < size; i++) { 17204 uid ^= textures[i].uid; 17205 uid = Math.imul(uid, 16777619); 17206 uid >>>= 0; 17207 } 17208 return cachedGroups[uid] || generateTextureBatchBindGroup(textures, size, uid, maxTextures); 17209} 17210function generateTextureBatchBindGroup(textures, size, key, maxTextures) { 17211 const bindGroupResources = {}; 17212 let bindIndex = 0; 17213 for (let i = 0; i < maxTextures; i++) { 17214 const texture = i < size ? textures[i] : Texture.EMPTY.source; 17215 bindGroupResources[bindIndex++] = texture.source; 17216 bindGroupResources[bindIndex++] = texture.style; 17217 } 17218 const bindGroup = new BindGroup(bindGroupResources); 17219 cachedGroups[key] = bindGroup; 17220 return bindGroup; 17221} 17222 17223class ViewableBuffer { 17224 constructor(sizeOrBuffer) { 17225 if (typeof sizeOrBuffer === "number") { 17226 this.rawBinaryData = new ArrayBuffer(sizeOrBuffer); 17227 } else if (sizeOrBuffer instanceof Uint8Array) { 17228 this.rawBinaryData = sizeOrBuffer.buffer; 17229 } else { 17230 this.rawBinaryData = sizeOrBuffer; 17231 } 17232 this.uint32View = new Uint32Array(this.rawBinaryData); 17233 this.float32View = new Float32Array(this.rawBinaryData); 17234 this.size = this.rawBinaryData.byteLength; 17235 } 17236 /** View on the raw binary data as a `Int8Array`. */ 17237 get int8View() { 17238 if (!this._int8View) { 17239 this._int8View = new Int8Array(this.rawBinaryData); 17240 } 17241 return this._int8View; 17242 } 17243 /** View on the raw binary data as a `Uint8Array`. */ 17244 get uint8View() { 17245 if (!this._uint8View) { 17246 this._uint8View = new Uint8Array(this.rawBinaryData); 17247 } 17248 return this._uint8View; 17249 } 17250 /** View on the raw binary data as a `Int16Array`. */ 17251 get int16View() { 17252 if (!this._int16View) { 17253 this._int16View = new Int16Array(this.rawBinaryData); 17254 } 17255 return this._int16View; 17256 } 17257 /** View on the raw binary data as a `Int32Array`. */ 17258 get int32View() { 17259 if (!this._int32View) { 17260 this._int32View = new Int32Array(this.rawBinaryData); 17261 } 17262 return this._int32View; 17263 } 17264 /** View on the raw binary data as a `Float64Array`. */ 17265 get float64View() { 17266 if (!this._float64Array) { 17267 this._float64Array = new Float64Array(this.rawBinaryData); 17268 } 17269 return this._float64Array; 17270 } 17271 /** View on the raw binary data as a `BigUint64Array`. */ 17272 get bigUint64View() { 17273 if (!this._bigUint64Array) { 17274 this._bigUint64Array = new BigUint64Array(this.rawBinaryData); 17275 } 17276 return this._bigUint64Array; 17277 } 17278 /** 17279 * Returns the view of the given type. 17280 * @param type - One of `int8`, `uint8`, `int16`, 17281 * `uint16`, `int32`, `uint32`, and `float32`. 17282 * @returns - typed array of given type 17283 */ 17284 view(type) { 17285 return this[`${type}View`]; 17286 } 17287 /** Destroys all buffer references. Do not use after calling this. */ 17288 destroy() { 17289 this.rawBinaryData = null; 17290 this.uint32View = null; 17291 this.float32View = null; 17292 this.uint16View = null; 17293 this._int8View = null; 17294 this._uint8View = null; 17295 this._int16View = null; 17296 this._int32View = null; 17297 this._float64Array = null; 17298 this._bigUint64Array = null; 17299 } 17300 /** 17301 * Returns the size of the given type in bytes. 17302 * @param type - One of `int8`, `uint8`, `int16`, 17303 * `uint16`, `int32`, `uint32`, and `float32`. 17304 * @returns - size of the type in bytes 17305 */ 17306 static sizeOf(type) { 17307 switch (type) { 17308 case "int8": 17309 case "uint8": 17310 return 1; 17311 case "int16": 17312 case "uint16": 17313 return 2; 17314 case "int32": 17315 case "uint32": 17316 case "float32": 17317 return 4; 17318 default: 17319 throw new Error(`${type} isn't a valid view type`); 17320 } 17321 } 17322} 17323 17324function fastCopy(sourceBuffer, destinationBuffer) { 17325 const lengthDouble = sourceBuffer.byteLength / 8 | 0; 17326 const sourceFloat64View = new Float64Array(sourceBuffer, 0, lengthDouble); 17327 const destinationFloat64View = new Float64Array(destinationBuffer, 0, lengthDouble); 17328 destinationFloat64View.set(sourceFloat64View); 17329 const remainingBytes = sourceBuffer.byteLength - lengthDouble * 8; 17330 if (remainingBytes > 0) { 17331 const sourceUint8View = new Uint8Array(sourceBuffer, lengthDouble * 8, remainingBytes); 17332 const destinationUint8View = new Uint8Array(destinationBuffer, lengthDouble * 8, remainingBytes); 17333 destinationUint8View.set(sourceUint8View); 17334 } 17335} 17336 17337const BLEND_TO_NPM = { 17338 normal: "normal-npm", 17339 add: "add-npm", 17340 screen: "screen-npm" 17341}; 17342var STENCIL_MODES = /* @__PURE__ */ ((STENCIL_MODES2) => { 17343 STENCIL_MODES2[STENCIL_MODES2["DISABLED"] = 0] = "DISABLED"; 17344 STENCIL_MODES2[STENCIL_MODES2["RENDERING_MASK_ADD"] = 1] = "RENDERING_MASK_ADD";
17345 STENCIL_MODES2[STENCIL_MODES2["MASK_ACTIVE"] = 2] = "MASK_ACTIVE"; 17346 STENCIL_MODES2[STENCIL_MODES2["INVERSE_MASK_ACTIVE"] = 3] = "INVERSE_MASK_ACTIVE"; 17347 STENCIL_MODES2[STENCIL_MODES2["RENDERING_MASK_REMOVE"] = 4] = "RENDERING_MASK_REMOVE"; 17348 STENCIL_MODES2[STENCIL_MODES2["NONE"] = 5] = "NONE"; 17349 return STENCIL_MODES2; 17350})(STENCIL_MODES || {}); 17351 17352function getAdjustedBlendModeBlend(blendMode, textureSource) { 17353 if (textureSource.alphaMode === "no-premultiply-alpha") { 17354 return BLEND_TO_NPM[blendMode] || blendMode; 17355 } 17356 return blendMode; 17357} 17358 17359const fragTemplate = [ 17360 "precision mediump float;", 17361 "void main(void){", 17362 "float test = 0.1;", 17363 "%forloop%", 17364 "gl_FragColor = vec4(0.0);", 17365 "}" 17366].join("\n"); 17367function generateIfTestSrc(maxIfs) { 17368 let src = ""; 17369 for (let i = 0; i < maxIfs; ++i) { 17370 if (i > 0) { 17371 src += "\nelse "; 17372 } 17373 if (i < maxIfs - 1) { 17374 src += `if(test == ${i}.0){}`; 17375 } 17376 } 17377 return src; 17378} 17379function checkMaxIfStatementsInShader(maxIfs, gl) { 17380 if (maxIfs === 0) { 17381 throw new Error("Invalid value of `0` passed to `checkMaxIfStatementsInShader`"); 17382 } 17383 const shader = gl.createShader(gl.FRAGMENT_SHADER); 17384 try { 17385 while (true) { 17386 const fragmentSrc = fragTemplate.replace(/%forloop%/gi, generateIfTestSrc(maxIfs)); 17387 gl.shaderSource(shader, fragmentSrc); 17388 gl.compileShader(shader); 17389 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 17390 maxIfs = maxIfs / 2 | 0; 17391 } else { 17392 break; 17393 } 17394 } 17395 } finally { 17396 gl.deleteShader(shader); 17397 } 17398 return maxIfs; 17399} 17400 17401let maxTexturesPerBatchCache = null; 17402function getMaxTexturesPerBatch() { 17403 if (maxTexturesPerBatchCache) 17404 return maxTexturesPerBatchCache; 17405 const gl = getTestContext(); 17406 maxTexturesPerBatchCache = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); 17407 maxTexturesPerBatchCache = checkMaxIfStatementsInShader( 17408 maxTexturesPerBatchCache, 17409 gl 17410 ); 17411 gl.getExtension("WEBGL_lose_context")?.loseContext(); 17412 return maxTexturesPerBatchCache; 17413} 17414 17415class BatchTextureArray { 17416 constructor() { 17417 /** Respective locations for textures. */ 17418 this.ids = /* @__PURE__ */ Object.create(null); 17419 this.textures = []; 17420 this.count = 0; 17421 } 17422 /** Clear the textures and their locations. */ 17423 clear() { 17424 for (let i = 0; i < this.count; i++) { 17425 const t = this.textures[i]; 17426 this.textures[i] = null; 17427 this.ids[t.uid] = null; 17428 } 17429 this.count = 0; 17430 } 17431} 17432 17433class Batch { 17434 constructor() { 17435 this.renderPipeId = "batch"; 17436 this.action = "startBatch"; 17437 // TODO - eventually this could be useful for flagging batches as dirty and then only rebuilding those ones 17438 // public elementStart = 0; 17439 // public elementSize = 0; 17440 // for drawing.. 17441 this.start = 0; 17442 this.size = 0; 17443 this.textures = new BatchTextureArray(); 17444 this.blendMode = "normal"; 17445 this.topology = "triangle-strip"; 17446 this.canBundle = true; 17447 } 17448 destroy() { 17449 this.textures = null; 17450 this.gpuBindGroup = null; 17451 this.bindGroup = null; 17452 this.batcher = null; 17453 } 17454} 17455const batchPool = []; 17456let batchPoolIndex = 0; 17457GlobalResourceRegistry.register({ 17458 clear: () => { 17459 if (batchPool.length > 0) { 17460 for (const item of batchPool) { 17461 if (item) 17462 item.destroy(); 17463 } 17464 } 17465 batchPool.length = 0; 17466 batchPoolIndex = 0; 17467 } 17468}); 17469function getBatchFromPool() { 17470 return batchPoolIndex > 0 ? batchPool[--batchPoolIndex] : new Batch(); 17471} 17472function returnBatchToPool(batch) { 17473 batchPool[batchPoolIndex++] = batch; 17474} 17475let BATCH_TICK = 0; 17476const _Batcher = class _Batcher { 17477 constructor(options) { 17478 /** unique id for this batcher */ 17479 this.uid = uid("batcher"); 17480 /** Indicates whether the batch data has been modified and needs updating. */ 17481 this.dirty = true; 17482 /** The current index of the batch being processed. */ 17483 this.batchIndex = 0; 17484 /** An array of all batches created during the current rendering process. */ 17485 this.batches = []; 17486 this._elements = []; 17487 options = { ..._Batcher.defaultOptions, ...options }; 17488 if (!options.maxTextures) { 17489 deprecation("v8.8.0", "maxTextures is a required option for Batcher now, please pass it in the options"); 17490 options.maxTextures = getMaxTexturesPerBatch(); 17491 } 17492 const { maxTextures, attributesInitialSize, indicesInitialSize } = options; 17493 this.attributeBuffer = new ViewableBuffer(attributesInitialSize * 4); 17494 this.indexBuffer = new Uint16Array(indicesInitialSize); 17495 this.maxTextures = maxTextures; 17496 } 17497 begin() { 17498 this.elementSize = 0; 17499 this.elementStart = 0; 17500 this.indexSize = 0; 17501 this.attributeSize = 0; 17502 for (let i = 0; i < this.batchIndex; i++) { 17503 returnBatchToPool(this.batches[i]); 17504 } 17505 this.batchIndex = 0; 17506 this._batchIndexStart = 0; 17507 this._batchIndexSize = 0; 17508 this.dirty = true; 17509 } 17510 add(batchableObject) { 17511 this._elements[this.elementSize++] = batchableObject; 17512 batchableObject._indexStart = this.indexSize; 17513 batchableObject._attributeStart = this.attributeSize; 17514 batchableObject._batcher = this; 17515 this.indexSize += batchableObject.indexSize; 17516 this.attributeSize += batchableObject.attributeSize * this.vertexSize; 17517 } 17518 checkAndUpdateTexture(batchableObject, texture) { 17519 const textureId = batchableObject._batch.textures.ids[texture._source.uid]; 17520 if (!textureId && textureId !== 0) 17521 return false;
17522 batchableObject._textureId = textureId; 17523 batchableObject.texture = texture; 17524 return true; 17525 } 17526 updateElement(batchableObject) { 17527 this.dirty = true; 17528 const attributeBuffer = this.attributeBuffer; 17529 if (batchableObject.packAsQuad) { 17530 this.packQuadAttributes( 17531 batchableObject, 17532 attributeBuffer.float32View, 17533 attributeBuffer.uint32View, 17534 batchableObject._attributeStart, 17535 batchableObject._textureId 17536 ); 17537 } else { 17538 this.packAttributes( 17539 batchableObject, 17540 attributeBuffer.float32View, 17541 attributeBuffer.uint32View, 17542 batchableObject._attributeStart, 17543 batchableObject._textureId 17544 ); 17545 } 17546 } 17547 /** 17548 * breaks the batcher. This happens when a batch gets too big, 17549 * or we need to switch to a different type of rendering (a filter for example) 17550 * @param instructionSet 17551 */ 17552 break(instructionSet) { 17553 const elements = this._elements; 17554 if (!elements[this.elementStart]) 17555 return; 17556 let batch = getBatchFromPool(); 17557 let textureBatch = batch.textures; 17558 textureBatch.clear(); 17559 const firstElement = elements[this.elementStart]; 17560 let blendMode = getAdjustedBlendModeBlend(firstElement.blendMode, firstElement.texture._source); 17561 let topology = firstElement.topology; 17562 if (this.attributeSize * 4 > this.attributeBuffer.size) { 17563 this._resizeAttributeBuffer(this.attributeSize * 4); 17564 } 17565 if (this.indexSize > this.indexBuffer.length) { 17566 this._resizeIndexBuffer(this.indexSize); 17567 } 17568 const f32 = this.attributeBuffer.float32View; 17569 const u32 = this.attributeBuffer.uint32View; 17570 const indexBuffer = this.indexBuffer; 17571 let size = this._batchIndexSize; 17572 let start = this._batchIndexStart; 17573 let action = "startBatch"; 17574 const maxTextures = this.maxTextures; 17575 for (let i = this.elementStart; i < this.elementSize; ++i) { 17576 const element = elements[i]; 17577 elements[i] = null; 17578 const texture = element.texture; 17579 const source = texture._source; 17580 const adjustedBlendMode = getAdjustedBlendModeBlend(element.blendMode, source); 17581 const breakRequired = blendMode !== adjustedBlendMode || topology !== element.topology; 17582 if (source._batchTick === BATCH_TICK && !breakRequired) { 17583 element._textureId = source._textureBindLocation; 17584 size += element.indexSize; 17585 if (element.packAsQuad) { 17586 this.packQuadAttributes( 17587 element, 17588 f32, 17589 u32, 17590 element._attributeStart, 17591 element._textureId 17592 ); 17593 this.packQuadIndex( 17594 indexBuffer, 17595 element._indexStart, 17596 element._attributeStart / this.vertexSize 17597 ); 17598 } else { 17599 this.packAttributes( 17600 element, 17601 f32, 17602 u32, 17603 element._attributeStart, 17604 element._textureId 17605 ); 17606 this.packIndex( 17607 element, 17608 indexBuffer, 17609 element._indexStart, 17610 element._attributeStart / this.vertexSize 17611 ); 17612 } 17613 element._batch = batch; 17614 continue; 17615 } 17616 source._batchTick = BATCH_TICK; 17617 if (textureBatch.count >= maxTextures || breakRequired) { 17618 this._finishBatch( 17619 batch, 17620 start, 17621 size - start, 17622 textureBatch, 17623 blendMode, 17624 topology, 17625 instructionSet, 17626 action 17627 ); 17628 action = "renderBatch"; 17629 start = size; 17630 blendMode = adjustedBlendMode; 17631 topology = element.topology; 17632 batch = getBatchFromPool(); 17633 textureBatch = batch.textures; 17634 textureBatch.clear(); 17635 ++BATCH_TICK; 17636 } 17637 element._textureId = source._textureBindLocation = textureBatch.count; 17638 textureBatch.ids[source.uid] = textureBatch.count; 17639 textureBatch.textures[textureBatch.count++] = source; 17640 element._batch = batch; 17641 size += element.indexSize; 17642 if (element.packAsQuad) { 17643 this.packQuadAttributes( 17644 element, 17645 f32, 17646 u32, 17647 element._attributeStart, 17648 element._textureId 17649 ); 17650 this.packQuadIndex( 17651 indexBuffer, 17652 element._indexStart, 17653 element._attributeStart / this.vertexSize 17654 ); 17655 } else { 17656 this.packAttributes( 17657 element, 17658 f32, 17659 u32, 17660 element._attributeStart, 17661 element._textureId 17662 ); 17663 this.packIndex( 17664 element, 17665 indexBuffer, 17666 element._indexStart, 17667 element._attributeStart / this.vertexSize 17668 ); 17669 } 17670 } 17671 if (textureBatch.count > 0) { 17672 this._finishBatch( 17673 batch, 17674 start, 17675 size - start, 17676 textureBatch, 17677 blendMode, 17678 topology, 17679 instructionSet, 17680 action 17681 ); 17682 start = size; 17683 ++BATCH_TICK; 17684 } 17685 this.elementStart = this.elementSize; 17686 this._batchIndexStart = start; 17687 this._batchIndexSize = size; 17688 } 17689 _finishBatch(batch, indexStart, indexSize, textureBatch, blendMode, topology, instructionSet, action) { 17690 batch.gpuBindGroup = null; 17691 batch.bindGroup = null; 17692 batch.action = action; 17693 batch.batcher = this; 17694 batch.textures = textureBatch; 17695 batch.blendMode = blendMode; 17696 batch.topology = topology; 17697 batch.start = indexStart; 17698 batch.size = indexSize; 17699 ++BATCH_TICK; 17700 this.batches[this.batchIndex++] = batch; 17701 instructionSet.add(batch); 17702 } 17703 finish(instructionSet) {
17704 this.break(instructionSet); 17705 } 17706 /** 17707 * Resizes the attribute buffer to the given size (1 = 1 float32) 17708 * @param size - the size in vertices to ensure (not bytes!) 17709 */ 17710 ensureAttributeBuffer(size) { 17711 if (size * 4 <= this.attributeBuffer.size) 17712 return; 17713 this._resizeAttributeBuffer(size * 4); 17714 } 17715 /** 17716 * Resizes the index buffer to the given size (1 = 1 float32) 17717 * @param size - the size in vertices to ensure (not bytes!) 17718 */ 17719 ensureIndexBuffer(size) { 17720 if (size <= this.indexBuffer.length) 17721 return; 17722 this._resizeIndexBuffer(size); 17723 } 17724 _resizeAttributeBuffer(size) { 17725 const newSize = Math.max(size, this.attributeBuffer.size * 2); 17726 const newArrayBuffer = new ViewableBuffer(newSize); 17727 fastCopy(this.attributeBuffer.rawBinaryData, newArrayBuffer.rawBinaryData); 17728 this.attributeBuffer = newArrayBuffer; 17729 } 17730 _resizeIndexBuffer(size) { 17731 const indexBuffer = this.indexBuffer; 17732 let newSize = Math.max(size, indexBuffer.length * 1.5); 17733 newSize += newSize % 2; 17734 const newIndexBuffer = newSize > 65535 ? new Uint32Array(newSize) : new Uint16Array(newSize); 17735 if (newIndexBuffer.BYTES_PER_ELEMENT !== indexBuffer.BYTES_PER_ELEMENT) { 17736 for (let i = 0; i < indexBuffer.length; i++) { 17737 newIndexBuffer[i] = indexBuffer[i]; 17738 } 17739 } else { 17740 fastCopy(indexBuffer.buffer, newIndexBuffer.buffer); 17741 } 17742 this.indexBuffer = newIndexBuffer; 17743 } 17744 packQuadIndex(indexBuffer, index, indicesOffset) { 17745 indexBuffer[index] = indicesOffset + 0; 17746 indexBuffer[index + 1] = indicesOffset + 1; 17747 indexBuffer[index + 2] = indicesOffset + 2; 17748 indexBuffer[index + 3] = indicesOffset + 0; 17749 indexBuffer[index + 4] = indicesOffset + 2; 17750 indexBuffer[index + 5] = indicesOffset + 3; 17751 } 17752 packIndex(element, indexBuffer, index, indicesOffset) { 17753 const indices = element.indices; 17754 const size = element.indexSize; 17755 const indexOffset = element.indexOffset; 17756 const attributeOffset = element.attributeOffset; 17757 for (let i = 0; i < size; i++) { 17758 indexBuffer[index++] = indicesOffset + indices[i + indexOffset] - attributeOffset; 17759 } 17760 } 17761 /** 17762 * Destroys the batch and its resources. 17763 * @param options - destruction options 17764 * @param options.shader - whether to destroy the associated shader 17765 */ 17766 destroy(options = {}) { 17767 if (this.batches === null) 17768 return; 17769 for (let i = 0; i < this.batchIndex; i++) { 17770 returnBatchToPool(this.batches[i]); 17771 } 17772 this.batches = null; 17773 this.geometry.destroy(true); 17774 this.geometry = null; 17775 if (options.shader) { 17776 this.shader?.destroy(); 17777 this.shader = null; 17778 } 17779 for (let i = 0; i < this._elements.length; i++) { 17780 if (this._elements[i]) 17781 this._elements[i]._batch = null; 17782 } 17783 this._elements = null; 17784 this.indexBuffer = null; 17785 this.attributeBuffer.destroy(); 17786 this.attributeBuffer = null; 17787 } 17788}; 17789_Batcher.defaultOptions = { 17790 maxTextures: null, 17791 attributesInitialSize: 4, 17792 indicesInitialSize: 6 17793}; 17794let Batcher = _Batcher; 17795 17796var BufferUsage = /* @__PURE__ */ ((BufferUsage2) => { 17797 BufferUsage2[BufferUsage2["MAP_READ"] = 1] = "MAP_READ"; 17798 BufferUsage2[BufferUsage2["MAP_WRITE"] = 2] = "MAP_WRITE"; 17799 BufferUsage2[BufferUsage2["COPY_SRC"] = 4] = "COPY_SRC"; 17800 BufferUsage2[BufferUsage2["COPY_DST"] = 8] = "COPY_DST"; 17801 BufferUsage2[BufferUsage2["INDEX"] = 16] = "INDEX"; 17802 BufferUsage2[BufferUsage2["VERTEX"] = 32] = "VERTEX"; 17803 BufferUsage2[BufferUsage2["UNIFORM"] = 64] = "UNIFORM"; 17804 BufferUsage2[BufferUsage2["STORAGE"] = 128] = "STORAGE"; 17805 BufferUsage2[BufferUsage2["INDIRECT"] = 256] = "INDIRECT"; 17806 BufferUsage2[BufferUsage2["QUERY_RESOLVE"] = 512] = "QUERY_RESOLVE"; 17807 BufferUsage2[BufferUsage2["STATIC"] = 1024] = "STATIC"; 17808 return BufferUsage2; 17809})(BufferUsage || {}); 17810 17811class Buffer extends EventEmitter { 17812 /** 17813 * Creates a new Buffer with the given options 17814 * @param options - the options for the buffer 17815 */ 17816 constructor(options) { 17817 let { data, size } = options; 17818 const { usage, label, shrinkToFit } = options; 17819 super(); 17820 /** 17821 * emits when the underlying buffer has changed shape (i.e. resized) 17822 * letting the renderer know that it needs to discard the old buffer on the GPU and create a new one 17823 * @event change 17824 */ 17825 /** 17826 * emits when the underlying buffer data has been updated. letting the renderer know 17827 * that it needs to update the buffer on the GPU 17828 * @event update 17829 */ 17830 /** 17831 * emits when the buffer is destroyed. letting the renderer know that it needs to destroy the buffer on the GPU 17832 * @event destroy 17833 */ 17834 /** @internal */ 17835 this._gpuData = /* @__PURE__ */ Object.create(null); 17836 /** @internal */ 17837 this._gcLastUsed = -1; 17838 /** If set to true, the buffer will be garbage collected automatically when it is not used. */ 17839 this.autoGarbageCollect = true; 17840 /** a unique id for this uniform group used through the renderer */ 17841 this.uid = uid("buffer"); 17842 /** 17843 * a resource type, used to identify how to handle it when its in a bind group / shader resource 17844 * @internal 17845 */ 17846 this._resourceType = "buffer"; 17847 /** 17848 * the resource id used internally by the renderer to build bind group keys 17849 * @internal 17850 */ 17851 this._resourceId = uid("resource"); 17852 /** 17853 * used internally to know if a uniform group was used in the last render pass 17854 * @internal 17855 */ 17856 this._touched = 0; 17857 /** @internal */ 17858 this._updateID = 1; 17859 this._dataInt32 = null; 17860 /** 17861 * should the GPU buffer be shrunk when the data becomes smaller? 17862 * changing this will cause the buffer to be destroyed and a new one created on the GPU 17863 * this can be expensive, especially if the buffer is already big enough!
17864 * setting this to false will prevent the buffer from being shrunk. This will yield better performance 17865 * if you are constantly setting data that is changing size often. 17866 * @default true 17867 */ 17868 this.shrinkToFit = true; 17869 /** 17870 * Has the buffer been destroyed? 17871 * @readonly 17872 */ 17873 this.destroyed = false; 17874 if (data instanceof Array) { 17875 data = new Float32Array(data); 17876 } 17877 this._data = data; 17878 size ?? (size = data?.byteLength); 17879 const mappedAtCreation = !!data; 17880 this.descriptor = { 17881 size, 17882 usage, 17883 mappedAtCreation, 17884 label 17885 }; 17886 this.shrinkToFit = shrinkToFit ?? true; 17887 } 17888 /** the data in the buffer */ 17889 get data() { 17890 return this._data; 17891 } 17892 set data(value) { 17893 this.setDataWithSize(value, value.length, true); 17894 } 17895 get dataInt32() { 17896 if (!this._dataInt32) { 17897 this._dataInt32 = new Int32Array(this.data.buffer); 17898 } 17899 return this._dataInt32; 17900 } 17901 /** whether the buffer is static or not */ 17902 get static() { 17903 return !!(this.descriptor.usage & BufferUsage.STATIC); 17904 } 17905 set static(value) { 17906 if (value) { 17907 this.descriptor.usage |= BufferUsage.STATIC; 17908 } else { 17909 this.descriptor.usage &= ~BufferUsage.STATIC; 17910 } 17911 } 17912 /** 17913 * Sets the data in the buffer to the given value. This will immediately update the buffer on the GPU. 17914 * If you only want to update a subset of the buffer, you can pass in the size of the data. 17915 * @param value - the data to set 17916 * @param size - the size of the data in bytes 17917 * @param syncGPU - should the buffer be updated on the GPU immediately? 17918 */ 17919 setDataWithSize(value, size, syncGPU) { 17920 this._updateID++; 17921 this._updateSize = size * value.BYTES_PER_ELEMENT; 17922 if (this._data === value) { 17923 if (syncGPU) 17924 this.emit("update", this); 17925 return; 17926 } 17927 const oldData = this._data; 17928 this._data = value; 17929 this._dataInt32 = null; 17930 if (!oldData || oldData.length !== value.length) { 17931 if (!this.shrinkToFit && oldData && value.byteLength < oldData.byteLength) { 17932 if (syncGPU) 17933 this.emit("update", this); 17934 } else { 17935 this.descriptor.size = value.byteLength; 17936 this._resourceId = uid("resource"); 17937 this.emit("change", this); 17938 } 17939 return; 17940 } 17941 if (syncGPU) 17942 this.emit("update", this); 17943 } 17944 /** 17945 * updates the buffer on the GPU to reflect the data in the buffer. 17946 * By default it will update the entire buffer. If you only want to update a subset of the buffer, 17947 * you can pass in the size of the buffer to update. 17948 * @param sizeInBytes - the new size of the buffer in bytes 17949 */ 17950 update(sizeInBytes) { 17951 this._updateSize = sizeInBytes ?? this._updateSize; 17952 this._updateID++; 17953 this.emit("update", this); 17954 } 17955 /** Unloads the buffer from the GPU */ 17956 unload() { 17957 this.emit("unload", this); 17958 for (const key in this._gpuData) { 17959 this._gpuData[key]?.destroy(); 17960 } 17961 this._gpuData = /* @__PURE__ */ Object.create(null); 17962 } 17963 /** Destroys the buffer */ 17964 destroy() { 17965 this.destroyed = true; 17966 this.unload(); 17967 this.emit("destroy", this); 17968 this.emit("change", this); 17969 this._data = null; 17970 this.descriptor = null; 17971 this.removeAllListeners(); 17972 } 17973} 17974 17975function ensureIsBuffer(buffer, index) { 17976 if (!(buffer instanceof Buffer)) { 17977 let usage = index ? BufferUsage.INDEX : BufferUsage.VERTEX; 17978 if (buffer instanceof Array) { 17979 if (index) { 17980 buffer = new Uint32Array(buffer); 17981 usage = BufferUsage.INDEX | BufferUsage.COPY_DST; 17982 } else { 17983 buffer = new Float32Array(buffer); 17984 usage = BufferUsage.VERTEX | BufferUsage.COPY_DST; 17985 } 17986 } 17987 buffer = new Buffer({ 17988 data: buffer, 17989 label: index ? "index-mesh-buffer" : "vertex-mesh-buffer", 17990 usage 17991 }); 17992 } 17993 return buffer; 17994} 17995 17996function getGeometryBounds(geometry, attributeId, bounds) { 17997 const attribute = geometry.getAttribute(attributeId); 17998 if (!attribute) { 17999 bounds.minX = 0; 18000 bounds.minY = 0; 18001 bounds.maxX = 0; 18002 bounds.maxY = 0; 18003 return bounds; 18004 } 18005 const data = attribute.buffer.data; 18006 let minX = Infinity; 18007 let minY = Infinity; 18008 let maxX = -Infinity; 18009 let maxY = -Infinity; 18010 const byteSize = data.BYTES_PER_ELEMENT; 18011 const offset = (attribute.offset || 0) / byteSize; 18012 const stride = (attribute.stride || 2 * 4) / byteSize; 18013 for (let i = offset; i < data.length; i += stride) { 18014 const x = data[i]; 18015 const y = data[i + 1]; 18016 if (x > maxX) 18017 maxX = x; 18018 if (y > maxY) 18019 maxY = y; 18020 if (x < minX) 18021 minX = x; 18022 if (y < minY) 18023 minY = y; 18024 } 18025 bounds.minX = minX; 18026 bounds.minY = minY; 18027 bounds.maxX = maxX; 18028 bounds.maxY = maxY; 18029 return bounds; 18030} 18031 18032function ensureIsAttribute(attribute) { 18033 if (attribute instanceof Buffer || Array.isArray(attribute) || attribute.BYTES_PER_ELEMENT) { 18034 attribute = { 18035 buffer: attribute 18036 }; 18037 } 18038 attribute.buffer = ensureIsBuffer(attribute.buffer, false); 18039 return attribute; 18040}
18041class Geometry extends EventEmitter { 18042 /** 18043 * Create a new instance of a geometry 18044 * @param options - The options for the geometry. 18045 */ 18046 constructor(options = {}) { 18047 super(); 18048 /** @internal */ 18049 this._gpuData = /* @__PURE__ */ Object.create(null); 18050 /** If set to true, the resource will be garbage collected automatically when it is not used. */ 18051 this.autoGarbageCollect = true; 18052 /** @internal */ 18053 this._gcLastUsed = -1; 18054 /** The unique id of the geometry. */ 18055 this.uid = uid("geometry"); 18056 /** 18057 * the layout key will be generated by WebGPU all geometries that have the same structure 18058 * will have the same layout key. This is used to cache the pipeline layout 18059 * @internal 18060 */ 18061 this._layoutKey = 0; 18062 /** the instance count of the geometry to draw */ 18063 this.instanceCount = 1; 18064 this._bounds = new Bounds(); 18065 this._boundsDirty = true; 18066 const { attributes, indexBuffer, topology } = options; 18067 this.buffers = []; 18068 this.attributes = {}; 18069 if (attributes) { 18070 for (const i in attributes) { 18071 this.addAttribute(i, attributes[i]); 18072 } 18073 } 18074 this.instanceCount = options.instanceCount ?? 1; 18075 if (indexBuffer) { 18076 this.addIndex(indexBuffer); 18077 } 18078 this.topology = topology || "triangle-list"; 18079 } 18080 onBufferUpdate() { 18081 this._boundsDirty = true; 18082 this.emit("update", this); 18083 } 18084 /** 18085 * Returns the requested attribute. 18086 * @param id - The name of the attribute required 18087 * @returns - The attribute requested. 18088 */ 18089 getAttribute(id) { 18090 return this.attributes[id]; 18091 } 18092 /** 18093 * Returns the index buffer 18094 * @returns - The index buffer. 18095 */ 18096 getIndex() { 18097 return this.indexBuffer; 18098 } 18099 /** 18100 * Returns the requested buffer. 18101 * @param id - The name of the buffer required. 18102 * @returns - The buffer requested. 18103 */ 18104 getBuffer(id) { 18105 return this.getAttribute(id).buffer; 18106 } 18107 /** 18108 * Used to figure out how many vertices there are in this geometry 18109 * @returns the number of vertices in the geometry 18110 */ 18111 getSize() { 18112 for (const i in this.attributes) { 18113 const attribute = this.attributes[i]; 18114 const buffer = attribute.buffer; 18115 return buffer.data.length / (attribute.stride / 4 || attribute.size); 18116 } 18117 return 0; 18118 } 18119 /** 18120 * Adds an attribute to the geometry. 18121 * @param name - The name of the attribute to add. 18122 * @param attributeOption - The attribute option to add. 18123 */ 18124 addAttribute(name, attributeOption) { 18125 const attribute = ensureIsAttribute(attributeOption); 18126 const bufferIndex = this.buffers.indexOf(attribute.buffer); 18127 if (bufferIndex === -1) { 18128 this.buffers.push(attribute.buffer); 18129 attribute.buffer.on("update", this.onBufferUpdate, this); 18130 attribute.buffer.on("change", this.onBufferUpdate, this); 18131 } 18132 this.attributes[name] = attribute; 18133 } 18134 /** 18135 * Adds an index buffer to the geometry. 18136 * @param indexBuffer - The index buffer to add. Can be a Buffer, TypedArray, or an array of numbers. 18137 */ 18138 addIndex(indexBuffer) { 18139 this.indexBuffer = ensureIsBuffer(indexBuffer, true); 18140 this.buffers.push(this.indexBuffer); 18141 } 18142 /** Returns the bounds of the geometry. */ 18143 get bounds() { 18144 if (!this._boundsDirty) 18145 return this._bounds; 18146 this._boundsDirty = false; 18147 return getGeometryBounds(this, "aPosition", this._bounds); 18148 } 18149 /** Unloads the geometry from the GPU. */ 18150 unload() { 18151 this.emit("unload", this); 18152 for (const key in this._gpuData) { 18153 this._gpuData[key]?.destroy(); 18154 } 18155 this._gpuData = /* @__PURE__ */ Object.create(null); 18156 } 18157 /** 18158 * destroys the geometry. 18159 * @param destroyBuffers - destroy the buffers associated with this geometry 18160 */ 18161 destroy(destroyBuffers = false) { 18162 this.emit("destroy", this); 18163 this.removeAllListeners(); 18164 if (destroyBuffers) { 18165 this.buffers.forEach((buffer) => buffer.destroy()); 18166 } 18167 this.unload(); 18168 this.indexBuffer?.destroy(); 18169 this.attributes = null; 18170 this.buffers = null; 18171 this.indexBuffer = null; 18172 this._bounds = null; 18173 } 18174} 18175 18176const placeHolderBufferData = new Float32Array(1); 18177const placeHolderIndexData = new Uint32Array(1); 18178class BatchGeometry extends Geometry { 18179 constructor() { 18180 const vertexSize = 6; 18181 const attributeBuffer = new Buffer({ 18182 data: placeHolderBufferData, 18183 label: "attribute-batch-buffer", 18184 usage: BufferUsage.VERTEX | BufferUsage.COPY_DST, 18185 shrinkToFit: false 18186 }); 18187 const indexBuffer = new Buffer({ 18188 data: placeHolderIndexData, 18189 label: "index-batch-buffer", 18190 usage: BufferUsage.INDEX | BufferUsage.COPY_DST, 18191 // | BufferUsage.STATIC,
18192 shrinkToFit: false 18193 }); 18194 const stride = vertexSize * 4; 18195 super({ 18196 attributes: { 18197 aPosition: { 18198 buffer: attributeBuffer, 18199 format: "float32x2", 18200 stride, 18201 offset: 0 18202 }, 18203 aUV: { 18204 buffer: attributeBuffer, 18205 format: "float32x2", 18206 stride, 18207 offset: 2 * 4 18208 }, 18209 aColor: { 18210 buffer: attributeBuffer, 18211 format: "unorm8x4", 18212 stride, 18213 offset: 4 * 4 18214 }, 18215 aTextureIdAndRound: { 18216 buffer: attributeBuffer, 18217 format: "uint16x2", 18218 stride, 18219 offset: 5 * 4 18220 } 18221 }, 18222 indexBuffer 18223 }); 18224 } 18225} 18226 18227function addBits(srcParts, parts, name) { 18228 if (srcParts) { 18229 for (const i in srcParts) { 18230 const id = i.toLocaleLowerCase(); 18231 const part = parts[id]; 18232 if (part) { 18233 let sanitisedPart = srcParts[i]; 18234 if (i === "header") { 18235 sanitisedPart = sanitisedPart.replace(/@in\s+[^;]+;\s*/g, "").replace(/@out\s+[^;]+;\s*/g, ""); 18236 } 18237 if (name) { 18238 part.push(`//----${name}----//`); 18239 } 18240 part.push(sanitisedPart); 18241 } else { 18242 warn(`${i} placement hook does not exist in shader`); 18243 } 18244 } 18245 } 18246} 18247 18248const findHooksRx = /\{\{(.*?)\}\}/g; 18249function compileHooks(programSrc) { 18250 const parts = {}; 18251 const partMatches = programSrc.match(findHooksRx)?.map((hook) => hook.replace(/[{()}]/g, "")) ?? []; 18252 partMatches.forEach((hook) => { 18253 parts[hook] = []; 18254 }); 18255 return parts; 18256} 18257 18258function extractInputs(fragmentSource, out) { 18259 let match; 18260 const regex = /@in\s+([^;]+);/g; 18261 while ((match = regex.exec(fragmentSource)) !== null) { 18262 out.push(match[1]); 18263 } 18264} 18265function compileInputs(fragments, template, sort = false) { 18266 const results = []; 18267 extractInputs(template, results); 18268 fragments.forEach((fragment) => { 18269 if (fragment.header) { 18270 extractInputs(fragment.header, results); 18271 } 18272 }); 18273 const mainInput = results; 18274 if (sort) { 18275 mainInput.sort(); 18276 } 18277 const finalString = mainInput.map((inValue, i) => ` @location(${i}) ${inValue},`).join("\n"); 18278 let cleanedString = template.replace(/@in\s+[^;]+;\s*/g, ""); 18279 cleanedString = cleanedString.replace("{{in}}", ` 18280${finalString} 18281`); 18282 return cleanedString; 18283} 18284 18285function extractOutputs(fragmentSource, out) { 18286 let match; 18287 const regex = /@out\s+([^;]+);/g; 18288 while ((match = regex.exec(fragmentSource)) !== null) { 18289 out.push(match[1]); 18290 } 18291} 18292function extractVariableName(value) { 18293 const regex = /\b(\w+)\s*:/g; 18294 const match = regex.exec(value); 18295 return match ? match[1] : ""; 18296} 18297function stripVariable(value) { 18298 const regex = /@.*?\s+/g; 18299 return value.replace(regex, ""); 18300} 18301function compileOutputs(fragments, template) { 18302 const results = []; 18303 extractOutputs(template, results); 18304 fragments.forEach((fragment) => { 18305 if (fragment.header) { 18306 extractOutputs(fragment.header, results); 18307 } 18308 }); 18309 let index = 0; 18310 const mainStruct = results.sort().map((inValue) => { 18311 if (inValue.indexOf("builtin") > -1) { 18312 return inValue; 18313 } 18314 return `@location(${index++}) ${inValue}`; 18315 }).join(",\n"); 18316 const mainStart = results.sort().map((inValue) => ` var ${stripVariable(inValue)};`).join("\n"); 18317 const mainEnd = `return VSOutput( 18318 ${results.sort().map((inValue) => ` ${extractVariableName(inValue)}`).join(",\n")});`; 18319 let compiledCode = template.replace(/@out\s+[^;]+;\s*/g, ""); 18320 compiledCode = compiledCode.replace("{{struct}}", ` 18321${mainStruct} 18322`); 18323 compiledCode = compiledCode.replace("{{start}}", ` 18324${mainStart} 18325`); 18326 compiledCode = compiledCode.replace("{{return}}", ` 18327${mainEnd} 18328`); 18329 return compiledCode; 18330} 18331 18332function injectBits(templateSrc, fragmentParts) { 18333 let out = templateSrc; 18334 for (const i in fragmentParts) { 18335 const parts = fragmentParts[i]; 18336 const toInject = parts.join("\n"); 18337 if (toInject.length) { 18338 out = out.replace(`{{${i}}}`, `//-----${i} START-----// 18339${parts.join("\n")} 18340//----${i} FINISH----//`); 18341 } else { 18342 out = out.replace(`{{${i}}}`, ""); 18343 } 18344 } 18345 return out; 18346} 18347 18348const cacheMap = /* @__PURE__ */ Object.create(null); 18349const bitCacheMap = /* @__PURE__ */ new Map(); 18350let CACHE_UID = 0; 18351function compileHighShader({ 18352 template, 18353 bits 18354}
vendor: 9,896 bytes, lines 18354-18782
18354) { 18355 const cacheId = generateCacheId(template, bits); 18356 if (cacheMap[cacheId]) 18357 return cacheMap[cacheId]; 18358 const { vertex, fragment } = compileInputsAndOutputs(template, bits); 18359 cacheMap[cacheId] = compileBits(vertex, fragment, bits); 18360 return cacheMap[cacheId]; 18361} 18362function compileHighShaderGl({ 18363 template, 18364 bits 18365}) { 18366 const cacheId = generateCacheId(template, bits); 18367 if (cacheMap[cacheId]) 18368 return cacheMap[cacheId]; 18369 cacheMap[cacheId] = compileBits(template.vertex, template.fragment, bits); 18370 return cacheMap[cacheId]; 18371} 18372function compileInputsAndOutputs(template, bits) { 18373 const vertexFragments = bits.map((shaderBit) => shaderBit.vertex).filter((v) => !!v); 18374 const fragmentFragments = bits.map((shaderBit) => shaderBit.fragment).filter((v) => !!v); 18375 let compiledVertex = compileInputs(vertexFragments, template.vertex, true); 18376 compiledVertex = compileOutputs(vertexFragments, compiledVertex); 18377 const compiledFragment = compileInputs(fragmentFragments, template.fragment, true); 18378 return { 18379 vertex: compiledVertex, 18380 fragment: compiledFragment 18381 }; 18382} 18383function generateCacheId(template, bits) { 18384 return bits.map((highFragment) => { 18385 if (!bitCacheMap.has(highFragment)) { 18386 bitCacheMap.set(highFragment, CACHE_UID++); 18387 } 18388 return bitCacheMap.get(highFragment); 18389 }).sort((a, b) => a - b).join("-") + template.vertex + template.fragment; 18390} 18391function compileBits(vertex, fragment, bits) { 18392 const vertexParts = compileHooks(vertex); 18393 const fragmentParts = compileHooks(fragment); 18394 bits.forEach((shaderBit) => { 18395 addBits(shaderBit.vertex, vertexParts, shaderBit.name); 18396 addBits(shaderBit.fragment, fragmentParts, shaderBit.name); 18397 }); 18398 return { 18399 vertex: injectBits(vertex, vertexParts), 18400 fragment: injectBits(fragment, fragmentParts) 18401 }; 18402} 18403 18404const vertexGPUTemplate = ( 18405 /* wgsl */ 18406 ` 18407 @in aPosition: vec2<f32>; 18408 @in aUV: vec2<f32>; 18409 18410 @out @builtin(position) vPosition: vec4<f32>; 18411 @out vUV : vec2<f32>; 18412 @out vColor : vec4<f32>; 18413 18414 {{header}} 18415 18416 struct VSOutput { 18417 {{struct}} 18418 }; 18419 18420 @vertex 18421 fn main( {{in}} ) -> VSOutput { 18422 18423 var worldTransformMatrix = globalUniforms.uWorldTransformMatrix; 18424 var modelMatrix = mat3x3<f32>( 18425 1.0, 0.0, 0.0, 18426 0.0, 1.0, 0.0, 18427 0.0, 0.0, 1.0 18428 ); 18429 var position = aPosition; 18430 var uv = aUV; 18431 18432 {{start}} 18433 18434 vColor = vec4<f32>(1., 1., 1., 1.); 18435 18436 {{main}} 18437 18438 vUV = uv; 18439 18440 var modelViewProjectionMatrix = globalUniforms.uProjectionMatrix * worldTransformMatrix * modelMatrix; 18441 18442 vPosition = vec4<f32>((modelViewProjectionMatrix * vec3<f32>(position, 1.0)).xy, 0.0, 1.0); 18443 18444 vColor *= globalUniforms.uWorldColorAlpha; 18445 18446 {{end}} 18447 18448 {{return}} 18449 }; 18450` 18451); 18452const fragmentGPUTemplate = ( 18453 /* wgsl */ 18454 ` 18455 @in vUV : vec2<f32>; 18456 @in vColor : vec4<f32>; 18457 18458 {{header}} 18459 18460 @fragment 18461 fn main( 18462 {{in}} 18463 ) -> @location(0) vec4<f32> { 18464 18465 {{start}} 18466 18467 var outColor:vec4<f32>; 18468 18469 {{main}} 18470 18471 var finalColor:vec4<f32> = outColor * vColor; 18472 18473 {{end}} 18474 18475 return finalColor; 18476 }; 18477` 18478); 18479const vertexGlTemplate = ( 18480 /* glsl */ 18481 ` 18482 in vec2 aPosition; 18483 in vec2 aUV; 18484 18485 out vec4 vColor; 18486 out vec2 vUV; 18487 18488 {{header}} 18489 18490 void main(void){ 18491 18492 mat3 worldTransformMatrix = uWorldTransformMatrix; 18493 mat3 modelMatrix = mat3( 18494 1.0, 0.0, 0.0, 18495 0.0, 1.0, 0.0, 18496 0.0, 0.0, 1.0 18497 ); 18498 vec2 position = aPosition; 18499 vec2 uv = aUV; 18500 18501 {{start}} 18502 18503 vColor = vec4(1.); 18504 18505 {{main}} 18506 18507 vUV = uv; 18508 18509 mat3 modelViewProjectionMatrix = uProjectionMatrix * worldTransformMatrix * modelMatrix; 18510 18511 gl_Position = vec4((modelViewProjectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); 18512 18513 vColor *= uWorldColorAlpha; 18514 18515 {{end}} 18516 } 18517` 18518); 18519const fragmentGlTemplate = ( 18520 /* glsl */ 18521 ` 18522 18523 in vec4 vColor; 18524 in vec2 vUV; 18525 18526 out vec4 finalColor; 18527 18528 {{header}} 18529 18530 void main(void) { 18531 18532 {{start}} 18533 18534 vec4 outColor; 18535 18536 {{main}} 18537 18538 finalColor = outColor * vColor; 18539 18540 {{end}} 18541 } 18542` 18543); 18544 18545const globalUniformsBit = { 18546 name: "global-uniforms-bit", 18547 vertex: { 18548 header: ( 18549 /* wgsl */ 18550 ` 18551 struct GlobalUniforms { 18552 uProjectionMatrix:mat3x3<f32>, 18553 uWorldTransformMatrix:mat3x3<f32>, 18554 uWorldColorAlpha: vec4<f32>, 18555 uResolution: vec2<f32>, 18556 } 18557 18558 @group(0) @binding(0) var<uniform> globalUniforms : GlobalUniforms; 18559 ` 18560 ) 18561 } 18562}; 18563const globalUniformsBitGl = { 18564 name: "global-uniforms-bit", 18565 vertex: { 18566 header: ( 18567 /* glsl */ 18568 ` 18569 uniform mat3 uProjectionMatrix; 18570 uniform mat3 uWorldTransformMatrix; 18571 uniform vec4 uWorldColorAlpha; 18572 uniform vec2 uResolution; 18573 ` 18574 ) 18575 } 18576}; 18577 18578function compileHighShaderGpuProgram({ bits, name }) { 18579 const source = compileHighShader({ 18580 template: { 18581 fragment: fragmentGPUTemplate, 18582 vertex: vertexGPUTemplate 18583 }, 18584 bits: [ 18585 globalUniformsBit, 18586 ...bits 18587 ] 18588 }); 18589 return GpuProgram.from({ 18590 name, 18591 vertex: { 18592 source: source.vertex, 18593 entryPoint: "main" 18594 }, 18595 fragment: { 18596 source: source.fragment, 18597 entryPoint: "main" 18598 } 18599 }); 18600} 18601function compileHighShaderGlProgram({ bits, name }) { 18602 return new GlProgram({ 18603 name, 18604 ...compileHighShaderGl({ 18605 template: { 18606 vertex: vertexGlTemplate, 18607 fragment: fragmentGlTemplate 18608 }, 18609 bits: [ 18610 globalUniformsBitGl, 18611 ...bits 18612 ] 18613 }) 18614 }); 18615} 18616 18617const colorBit = { 18618 name: "color-bit", 18619 vertex: { 18620 header: ( 18621 /* wgsl */ 18622 ` 18623 @in aColor: vec4<f32>; 18624 ` 18625 ), 18626 main: ( 18627 /* wgsl */ 18628 ` 18629 vColor *= vec4<f32>(aColor.rgb * aColor.a, aColor.a); 18630 ` 18631 ) 18632 } 18633}; 18634const colorBitGl = { 18635 name: "color-bit", 18636 vertex: { 18637 header: ( 18638 /* glsl */ 18639 ` 18640 in vec4 aColor; 18641 ` 18642 ), 18643 main: ( 18644 /* glsl */ 18645 ` 18646 vColor *= vec4(aColor.rgb * aColor.a, aColor.a); 18647 ` 18648 ) 18649 } 18650}; 18651 18652const textureBatchBitGpuCache = {}; 18653function generateBindingSrc(maxTextures) { 18654 const src = []; 18655 if (maxTextures === 1) { 18656 src.push("@group(1) @binding(0) var textureSource1: texture_2d<f32>;"); 18657 src.push("@group(1) @binding(1) var textureSampler1: sampler;"); 18658 } else { 18659 let bindingIndex = 0; 18660 for (let i = 0; i < maxTextures; i++) { 18661 src.push(`@group(1) @binding(${bindingIndex++}) var textureSource${i + 1}: texture_2d<f32>;`); 18662 src.push(`@group(1) @binding(${bindingIndex++}) var textureSampler${i + 1}: sampler;`); 18663 } 18664 } 18665 return src.join("\n"); 18666} 18667function generateSampleSrc(maxTextures) { 18668 const src = []; 18669 if (maxTextures === 1) { 18670 src.push("outColor = textureSampleGrad(textureSource1, textureSampler1, vUV, uvDx, uvDy);"); 18671 } else { 18672 src.push("switch vTextureId {"); 18673 for (let i = 0; i < maxTextures; i++) { 18674 if (i === maxTextures - 1) { 18675 src.push(` default:{`); 18676 } else { 18677 src.push(` case ${i}:{`); 18678 } 18679 src.push(` outColor = textureSampleGrad(textureSource${i + 1}, textureSampler${i + 1}, vUV, uvDx, uvDy);`); 18680 src.push(` break;}`); 18681 } 18682 src.push(`}`); 18683 } 18684 return src.join("\n"); 18685} 18686function generateTextureBatchBit(maxTextures) { 18687 if (!textureBatchBitGpuCache[maxTextures]) { 18688 textureBatchBitGpuCache[maxTextures] = { 18689 name: "texture-batch-bit", 18690 vertex: { 18691 header: ` 18692 @in aTextureIdAndRound: vec2<u32>; 18693 @out @interpolate(flat) vTextureId : u32; 18694 `, 18695 main: ` 18696 vTextureId = aTextureIdAndRound.y; 18697 `, 18698 end: ` 18699 if(aTextureIdAndRound.x == 1) 18700 { 18701 vPosition = vec4<f32>(roundPixels(vPosition.xy, globalUniforms.uResolution), vPosition.zw); 18702 } 18703 ` 18704 }, 18705 fragment: { 18706 header: ` 18707 @in @interpolate(flat) vTextureId: u32; 18708 18709 ${generateBindingSrc(maxTextures)} 18710 `, 18711 main: ` 18712 var uvDx = dpdx(vUV); 18713 var uvDy = dpdy(vUV); 18714 18715 ${generateSampleSrc(maxTextures)} 18716 ` 18717 } 18718 }; 18719 } 18720 return textureBatchBitGpuCache[maxTextures]; 18721} 18722const textureBatchBitGlCache = {}; 18723function generateSampleGlSrc(maxTextures) { 18724 const src = []; 18725 for (let i = 0; i < maxTextures; i++) { 18726 if (i > 0) { 18727 src.push("else"); 18728 } 18729 if (i < maxTextures - 1) { 18730 src.push(`if(vTextureId < ${i}.5)`); 18731 } 18732 src.push("{"); 18733 src.push(` outColor = texture(uTextures[${i}], vUV);`); 18734 src.push("}"); 18735 } 18736 return src.join("\n"); 18737} 18738function generateTextureBatchBitGl(maxTextures) { 18739 if (!textureBatchBitGlCache[maxTextures]) { 18740 textureBatchBitGlCache[maxTextures] = { 18741 name: "texture-batch-bit", 18742 vertex: { 18743 header: ` 18744 in vec2 aTextureIdAndRound; 18745 out float vTextureId; 18746 18747 `, 18748 main: ` 18749 vTextureId = aTextureIdAndRound.y; 18750 `, 18751 end: ` 18752 if(aTextureIdAndRound.x == 1.) 18753 { 18754 gl_Position.xy = roundPixels(gl_Position.xy, uResolution); 18755 } 18756 ` 18757 }, 18758 fragment: { 18759 header: ` 18760 in float vTextureId; 18761 18762 uniform sampler2D uTextures[${maxTextures}]; 18763 18764 `, 18765 main: ` 18766 18767 ${generateSampleGlSrc(maxTextures)} 18768 ` 18769 } 18770 }; 18771 } 18772 return textureBatchBitGlCache[maxTextures]; 18773} 18774 18775const roundPixelsBit = { 18776 name: "round-pixels-bit", 18777 vertex: { 18778 header: ( 18779 /* wgsl */ 18780 ` 18781 fn roundPixels(position: vec2<f32>, targetSize: vec2<f32>) -> vec2<f32> 18782 {
18783 return (floor(((position * 0.5 + 0.5) * targetSize) + 0.5) / targetSize) * 2.0 - 1.0; 18784 } 18785 ` 18786 ) 18787 } 18788}; 18789const roundPixelsBitGl = { 18790 name: "round-pixels-bit", 18791 vertex: { 18792 header: ( 18793 /* glsl */ 18794 ` 18795 vec2 roundPixels(vec2 position, vec2 targetSize) 18796 { 18797 return (floor(((position * 0.5 + 0.5) * targetSize) + 0.5) / targetSize) * 2.0 - 1.0; 18798 } 18799 ` 18800 ) 18801 } 18802}; 18803 18804const batchSamplersUniformGroupHash = {}; 18805function getBatchSamplersUniformGroup(maxTextures) { 18806 let batchSamplersUniformGroup = batchSamplersUniformGroupHash[maxTextures]; 18807 if (batchSamplersUniformGroup) 18808 return batchSamplersUniformGroup; 18809 const sampleValues = new Int32Array(maxTextures); 18810 for (let i = 0; i < maxTextures; i++) { 18811 sampleValues[i] = i; 18812 } 18813 batchSamplersUniformGroup = batchSamplersUniformGroupHash[maxTextures] = new UniformGroup({ 18814 uTextures: { value: sampleValues, type: `i32`, size: maxTextures } 18815 }, { isStatic: true }); 18816 return batchSamplersUniformGroup; 18817} 18818 18819class DefaultShader extends Shader { 18820 constructor(maxTextures) { 18821 const glProgram = compileHighShaderGlProgram({ 18822 name: "batch", 18823 bits: [ 18824 colorBitGl, 18825 generateTextureBatchBitGl(maxTextures), 18826 roundPixelsBitGl 18827 ] 18828 }); 18829 const gpuProgram = compileHighShaderGpuProgram({ 18830 name: "batch", 18831 bits: [ 18832 colorBit, 18833 generateTextureBatchBit(maxTextures), 18834 roundPixelsBit 18835 ] 18836 }); 18837 super({ 18838 glProgram, 18839 gpuProgram, 18840 resources: { 18841 batchSamplers: getBatchSamplersUniformGroup(maxTextures) 18842 } 18843 }); 18844 this.maxTextures = maxTextures; 18845 } 18846} 18847 18848let defaultShader = null; 18849const _DefaultBatcher = class _DefaultBatcher extends Batcher { 18850 constructor(options) { 18851 super(options); 18852 this.geometry = new BatchGeometry(); 18853 this.name = _DefaultBatcher.extension.name; 18854 /** The size of one attribute. 1 = 32 bit. x, y, u, v, color, textureIdAndRound -> total = 6 */ 18855 this.vertexSize = 6; 18856 defaultShader ?? (defaultShader = new DefaultShader(options.maxTextures)); 18857 this.shader = defaultShader; 18858 } 18859 /** 18860 * Packs the attributes of a DefaultBatchableMeshElement into the provided views. 18861 * @param element - The DefaultBatchableMeshElement to pack. 18862 * @param float32View - The Float32Array view to pack into. 18863 * @param uint32View - The Uint32Array view to pack into. 18864 * @param index - The starting index in the views. 18865 * @param textureId - The texture ID to use. 18866 */ 18867 packAttributes(element, float32View, uint32View, index, textureId) { 18868 const textureIdAndRound = textureId << 16 | element.roundPixels & 65535; 18869 const wt = element.transform; 18870 const a = wt.a; 18871 const b = wt.b; 18872 const c = wt.c; 18873 const d = wt.d; 18874 const tx = wt.tx; 18875 const ty = wt.ty; 18876 const { positions, uvs } = element; 18877 const argb = element.color; 18878 const offset = element.attributeOffset; 18879 const end = offset + element.attributeSize; 18880 for (let i = offset; i < end; i++) { 18881 const i2 = i * 2; 18882 const x = positions[i2]; 18883 const y = positions[i2 + 1]; 18884 float32View[index++] = a * x + c * y + tx; 18885 float32View[index++] = d * y + b * x + ty; 18886 float32View[index++] = uvs[i2]; 18887 float32View[index++] = uvs[i2 + 1]; 18888 uint32View[index++] = argb; 18889 uint32View[index++] = textureIdAndRound; 18890 } 18891 } 18892 /** 18893 * Packs the attributes of a DefaultBatchableQuadElement into the provided views. 18894 * @param element - The DefaultBatchableQuadElement to pack. 18895 * @param float32View - The Float32Array view to pack into. 18896 * @param uint32View - The Uint32Array view to pack into. 18897 * @param index - The starting index in the views. 18898 * @param textureId - The texture ID to use. 18899 */ 18900 packQuadAttributes(element, float32View, uint32View, index, textureId) { 18901 const texture = element.texture; 18902 const wt = element.transform; 18903 const a = wt.a; 18904 const b = wt.b; 18905 const c = wt.c; 18906 const d = wt.d; 18907 const tx = wt.tx; 18908 const ty = wt.ty; 18909 const bounds = element.bounds; 18910 const w0 = bounds.maxX; 18911 const w1 = bounds.minX; 18912 const h0 = bounds.maxY; 18913 const h1 = bounds.minY; 18914 const uvs = texture.uvs; 18915 const argb = element.color; 18916 const textureIdAndRound = textureId << 16 | element.roundPixels & 65535; 18917 float32View[index + 0] = a * w1 + c * h1 + tx; 18918 float32View[index + 1] = d * h1 + b * w1 + ty; 18919 float32View[index + 2] = uvs.x0; 18920 float32View[index + 3] = uvs.y0; 18921 uint32View[index + 4] = argb; 18922 uint32View[index + 5] = textureIdAndRound; 18923 float32View[index + 6] = a * w0 + c * h1 + tx; 18924 float32View[index + 7] = d * h1 + b * w0 + ty; 18925 float32View[index + 8] = uvs.x1; 18926 float32View[index + 9] = uvs.y1; 18927 uint32View[index + 10] = argb; 18928 uint32View[index + 11] = textureIdAndRound; 18929 float32View[index + 12] = a * w0 + c * h0 + tx; 18930 float32View[index + 13] = d * h0 + b * w0 + ty; 18931 float32View[index + 14] = uvs.x2; 18932 float32View[index + 15] = uvs.y2; 18933 uint32View[index + 16] = argb; 18934 uint32View[index + 17] = textureIdAndRound; 18935 float32View[index + 18] = a * w1 + c * h0 + tx; 18936 float32View[index + 19] = d * h0 + b * w1 + ty; 18937 float32View[index + 20] = uvs.x3; 18938 float32View[index + 21] = uvs.y3; 18939 uint32View[index + 22] = argb; 18940 uint32View[index + 23] = textureIdAndRound; 18941 } 18942 /** 18943 * Updates the maximum number of textures that can be used in the shader. 18944 * @param maxTextures - The maximum number of textures that can be used in the shader. 18945 * @internal 18946 */ 18947 _updateMaxTextures(maxTextures) { 18948 if (this.shader.maxTextures === maxTextures) 18949 return; 18950 defaultShader = new DefaultShader(maxTextures); 18951 this.shader = defaultShader; 18952 } 18953 destroy() { 18954 this.shader = null; 18955 super.destroy(); 18956 } 18957}; 18958/** @ignore */ 18959_DefaultBatcher.extension = { 18960 type: [ 18961 ExtensionType.Batcher 18962 ], 18963 name: "default" 18964}; 18965let DefaultBatcher = _DefaultBatcher; 18966 18967class GCManagedHash { 18968 constructor(options) { 18969 // Exposed directly for GC system access 18970 this.items = /* @__PURE__ */ Object.create(null); 18971 const { renderer, type, onUnload, priority, name } = options; 18972 this._renderer = renderer; 18973 renderer.gc.addResourceHash(this, "items", type, priority ?? 0); 18974 this._onUnload = onUnload; 18975 this.name = name; 18976 } 18977 /** 18978 * Add an item to the hash. No-op if already added. 18979 * @param item 18980 * @returns true if the item was added, false if it was already in the hash 18981 */ 18982 add(item) { 18983 if (this.items[item.uid]) 18984 return false;
vendor: 51,499 bytes, lines 18985-20653
18985 this.items[item.uid] = item; 18986 item.once("unload", this.remove, this); 18987 item._gcLastUsed = this._renderer.gc.now; 18988 return true; 18989 } 18990 remove(item, ...args) { 18991 if (!this.items[item.uid]) 18992 return; 18993 const gpuData = item._gpuData[this._renderer.uid]; 18994 if (!gpuData) 18995 return; 18996 this._onUnload?.(item, ...args); 18997 gpuData.destroy(); 18998 item._gpuData[this._renderer.uid] = null; 18999 this.items[item.uid] = null; 19000 } 19001 removeAll(...args) { 19002 Object.values(this.items).forEach((item) => item && this.remove(item, ...args)); 19003 } 19004 destroy(...args) { 19005 this.removeAll(...args); 19006 this.items = /* @__PURE__ */ Object.create(null); 19007 this._renderer = null; 19008 this._onUnload = null; 19009 } 19010} 19011 19012function buildUvs(vertices, verticesStride, verticesOffset, uvs, uvsOffset, uvsStride, size, matrix = null) { 19013 let index = 0; 19014 verticesOffset *= verticesStride; 19015 uvsOffset *= uvsStride; 19016 const a = matrix.a; 19017 const b = matrix.b; 19018 const c = matrix.c; 19019 const d = matrix.d; 19020 const tx = matrix.tx; 19021 const ty = matrix.ty; 19022 while (index < size) { 19023 const x = vertices[verticesOffset]; 19024 const y = vertices[verticesOffset + 1]; 19025 uvs[uvsOffset] = a * x + c * y + tx; 19026 uvs[uvsOffset + 1] = b * x + d * y + ty; 19027 uvsOffset += uvsStride; 19028 verticesOffset += verticesStride; 19029 index++; 19030 } 19031} 19032function buildSimpleUvs(uvs, uvsOffset, uvsStride, size) { 19033 let index = 0; 19034 uvsOffset *= uvsStride; 19035 while (index < size) { 19036 uvs[uvsOffset] = 0; 19037 uvs[uvsOffset + 1] = 0; 19038 uvsOffset += uvsStride; 19039 index++; 19040 } 19041} 19042 19043function transformVertices(vertices, m, offset, stride, size) { 19044 const a = m.a; 19045 const b = m.b; 19046 const c = m.c; 19047 const d = m.d; 19048 const tx = m.tx; 19049 const ty = m.ty; 19050 offset || (offset = 0); 19051 stride || (stride = 2); 19052 size || (size = vertices.length / stride - offset); 19053 let index = offset * stride; 19054 for (let i = 0; i < size; i++) { 19055 const x = vertices[index]; 19056 const y = vertices[index + 1]; 19057 vertices[index] = a * x + c * y + tx; 19058 vertices[index + 1] = b * x + d * y + ty; 19059 index += stride; 19060 } 19061} 19062 19063const identityMatrix = new Matrix(); 19064class BatchableGraphics { 19065 constructor() { 19066 this.packAsQuad = false; 19067 this.batcherName = "default"; 19068 this.topology = "triangle-list"; 19069 this.applyTransform = true; 19070 this.roundPixels = 0; 19071 this._batcher = null; 19072 this._batch = null; 19073 } 19074 get uvs() { 19075 return this.geometryData.uvs; 19076 } 19077 get positions() { 19078 return this.geometryData.vertices; 19079 } 19080 get indices() { 19081 return this.geometryData.indices; 19082 } 19083 get blendMode() { 19084 if (this.renderable && this.applyTransform) { 19085 return this.renderable.groupBlendMode; 19086 } 19087 return "normal"; 19088 } 19089 get color() { 19090 const rgb = this.baseColor; 19091 const bgr = rgb >> 16 | rgb & 65280 | (rgb & 255) << 16; 19092 const renderable = this.renderable; 19093 if (renderable) { 19094 return multiplyHexColors(bgr, renderable.groupColor) + (this.alpha * renderable.groupAlpha * 255 << 24); 19095 } 19096 return bgr + (this.alpha * 255 << 24); 19097 } 19098 get transform() { 19099 return this.renderable?.groupTransform || identityMatrix; 19100 } 19101 copyTo(gpuBuffer) { 19102 gpuBuffer.indexOffset = this.indexOffset; 19103 gpuBuffer.indexSize = this.indexSize; 19104 gpuBuffer.attributeOffset = this.attributeOffset; 19105 gpuBuffer.attributeSize = this.attributeSize; 19106 gpuBuffer.baseColor = this.baseColor; 19107 gpuBuffer.alpha = this.alpha; 19108 gpuBuffer.texture = this.texture; 19109 gpuBuffer.geometryData = this.geometryData; 19110 gpuBuffer.topology = this.topology; 19111 } 19112 reset() { 19113 this.applyTransform = true; 19114 this.renderable = null; 19115 this.topology = "triangle-list"; 19116 } 19117 destroy() { 19118 this.renderable = null; 19119 this.texture = null; 19120 this.geometryData = null; 19121 this._batcher = null; 19122 this._batch = null; 19123 } 19124} 19125 19126const buildCircle = { 19127 extension: { 19128 type: ExtensionType.ShapeBuilder, 19129 name: "circle" 19130 }, 19131 build(shape, points) { 19132 let x; 19133 let y; 19134 let dx; 19135 let dy; 19136 let rx; 19137 let ry; 19138 if (shape.type === "circle") { 19139 const circle = shape; 19140 rx = ry = circle.radius; 19141 if (rx <= 0) { 19142 return false; 19143 } 19144 x = circle.x; 19145 y = circle.y; 19146 dx = dy = 0; 19147 } else if (shape.type === "ellipse") { 19148 const ellipse = shape; 19149 rx = ellipse.halfWidth; 19150 ry = ellipse.halfHeight; 19151 if (rx <= 0 || ry <= 0) { 19152 return false; 19153 } 19154 x = ellipse.x; 19155 y = ellipse.y; 19156 dx = dy = 0; 19157 } else { 19158 const roundedRect = shape; 19159 const halfWidth = roundedRect.width / 2; 19160 const halfHeight = roundedRect.height / 2; 19161 x = roundedRect.x + halfWidth; 19162 y = roundedRect.y + halfHeight; 19163 rx = ry = Math.max(0, Math.min(roundedRect.radius, Math.min(halfWidth, halfHeight))); 19164 dx = halfWidth - rx; 19165 dy = halfHeight - ry; 19166 } 19167 if (dx < 0 || dy < 0) { 19168 return false; 19169 } 19170 const n = Math.ceil(2.3 * Math.sqrt(rx + ry)); 19171 const m = n * 8 + (dx ? 4 : 0) + (dy ? 4 : 0); 19172 if (m === 0) { 19173 return false; 19174 } 19175 if (n === 0) { 19176 points[0] = points[6] = x + dx; 19177 points[1] = points[3] = y + dy; 19178 points[2] = points[4] = x - dx; 19179 points[5] = points[7] = y - dy; 19180 return true; 19181 } 19182 let j1 = 0; 19183 let j2 = n * 4 + (dx ? 2 : 0) + 2; 19184 let j3 = j2; 19185 let j4 = m; 19186 let x0 = dx + rx; 19187 let y0 = dy; 19188 let x1 = x + x0; 19189 let x2 = x - x0; 19190 let y1 = y + y0; 19191 points[j1++] = x1; 19192 points[j1++] = y1; 19193 points[--j2] = y1; 19194 points[--j2] = x2; 19195 if (dy) { 19196 const y22 = y - y0; 19197 points[j3++] = x2; 19198 points[j3++] = y22; 19199 points[--j4] = y22; 19200 points[--j4] = x1; 19201 } 19202 for (let i = 1; i < n; i++) { 19203 const a = Math.PI / 2 * (i / n); 19204 const x02 = dx + Math.cos(a) * rx; 19205 const y02 = dy + Math.sin(a) * ry; 19206 const x12 = x + x02; 19207 const x22 = x - x02; 19208 const y12 = y + y02; 19209 const y22 = y - y02; 19210 points[j1++] = x12; 19211 points[j1++] = y12; 19212 points[--j2] = y12; 19213 points[--j2] = x22; 19214 points[j3++] = x22; 19215 points[j3++] = y22; 19216 points[--j4] = y22; 19217 points[--j4] = x12; 19218 } 19219 x0 = dx; 19220 y0 = dy + ry; 19221 x1 = x + x0; 19222 x2 = x - x0; 19223 y1 = y + y0; 19224 const y2 = y - y0; 19225 points[j1++] = x1; 19226 points[j1++] = y1; 19227 points[--j4] = y2; 19228 points[--j4] = x1; 19229 if (dx) { 19230 points[j1++] = x2; 19231 points[j1++] = y1; 19232 points[--j4] = y2; 19233 points[--j4] = x2; 19234 } 19235 return true; 19236 }, 19237 triangulate(points, vertices, verticesStride, verticesOffset, indices, indicesOffset) { 19238 if (points.length === 0) { 19239 return; 19240 } 19241 let centerX = 0; 19242 let centerY = 0; 19243 for (let i = 0; i < points.length; i += 2) { 19244 centerX += points[i]; 19245 centerY += points[i + 1]; 19246 } 19247 centerX /= points.length / 2; 19248 centerY /= points.length / 2; 19249 let count = verticesOffset; 19250 vertices[count * verticesStride] = centerX; 19251 vertices[count * verticesStride + 1] = centerY; 19252 const centerIndex = count++; 19253 for (let i = 0; i < points.length; i += 2) { 19254 vertices[count * verticesStride] = points[i]; 19255 vertices[count * verticesStride + 1] = points[i + 1]; 19256 if (i > 0) { 19257 indices[indicesOffset++] = count; 19258 indices[indicesOffset++] = centerIndex; 19259 indices[indicesOffset++] = count - 1; 19260 } 19261 count++; 19262 } 19263 indices[indicesOffset++] = centerIndex + 1; 19264 indices[indicesOffset++] = centerIndex; 19265 indices[indicesOffset++] = count - 1; 19266 } 19267}; 19268const buildEllipse = { ...buildCircle, extension: { ...buildCircle.extension, name: "ellipse" } }; 19269const buildRoundedRectangle = { ...buildCircle, extension: { ...buildCircle.extension, name: "roundedRectangle" } }; 19270 19271const closePointEps = 1e-4; 19272const curveEps = 1e-4; 19273 19274function getOrientationOfPoints(points) { 19275 const m = points.length; 19276 if (m < 6) { 19277 return 1; 19278 } 19279 let area = 0; 19280 for (let i = 0, x1 = points[m - 2], y1 = points[m - 1]; i < m; i += 2) { 19281 const x2 = points[i]; 19282 const y2 = points[i + 1]; 19283 area += (x2 - x1) * (y2 + y1); 19284 x1 = x2; 19285 y1 = y2; 19286 } 19287 if (area < 0) { 19288 return -1; 19289 } 19290 return 1; 19291} 19292 19293function square(x, y, nx, ny, innerWeight, outerWeight, clockwise, verts) { 19294 const ix = x - nx * innerWeight; 19295 const iy = y - ny * innerWeight; 19296 const ox = x + nx * outerWeight; 19297 const oy = y + ny * outerWeight; 19298 let exx; 19299 let eyy; 19300 if (clockwise) { 19301 exx = ny; 19302 eyy = -nx; 19303 } else { 19304 exx = -ny; 19305 eyy = nx; 19306 } 19307 const eix = ix + exx; 19308 const eiy = iy + eyy; 19309 const eox = ox + exx; 19310 const eoy = oy + eyy; 19311 verts.push(eix, eiy); 19312 verts.push(eox, eoy); 19313 return 2; 19314} 19315function round(cx, cy, sx, sy, ex, ey, verts, clockwise) { 19316 const cx2p0x = sx - cx; 19317 const cy2p0y = sy - cy; 19318 let angle0 = Math.atan2(cx2p0x, cy2p0y); 19319 let angle1 = Math.atan2(ex - cx, ey - cy); 19320 if (clockwise && angle0 < angle1) { 19321 angle0 += Math.PI * 2; 19322 } else if (!clockwise && angle0 > angle1) { 19323 angle1 += Math.PI * 2; 19324 } 19325 let startAngle = angle0; 19326 const angleDiff = angle1 - angle0; 19327 const absAngleDiff = Math.abs(angleDiff); 19328 const radius = Math.sqrt(cx2p0x * cx2p0x + cy2p0y * cy2p0y); 19329 const segCount = (15 * absAngleDiff * Math.sqrt(radius) / Math.PI >> 0) + 1; 19330 const angleInc = angleDiff / segCount; 19331 startAngle += angleInc; 19332 if (clockwise) { 19333 verts.push(cx, cy); 19334 verts.push(sx, sy); 19335 for (let i = 1, angle = startAngle; i < segCount; i++, angle += angleInc) { 19336 verts.push(cx, cy); 19337 verts.push( 19338 cx + Math.sin(angle) * radius, 19339 cy + Math.cos(angle) * radius 19340 ); 19341 } 19342 verts.push(cx, cy); 19343 verts.push(ex, ey); 19344 } else { 19345 verts.push(sx, sy); 19346 verts.push(cx, cy); 19347 for (let i = 1, angle = startAngle; i < segCount; i++, angle += angleInc) { 19348 verts.push( 19349 cx + Math.sin(angle) * radius, 19350 cy + Math.cos(angle) * radius 19351 ); 19352 verts.push(cx, cy); 19353 } 19354 verts.push(ex, ey); 19355 verts.push(cx, cy); 19356 } 19357 return segCount * 2; 19358} 19359function buildLine(points, lineStyle, flipAlignment, closed, vertices, indices) { 19360 const eps = closePointEps; 19361 if (points.length === 0) { 19362 return; 19363 } 19364 const style = lineStyle; 19365 let alignment = style.alignment; 19366 if (lineStyle.alignment !== 0.5) { 19367 let orientation = getOrientationOfPoints(points); 19368 alignment = (alignment - 0.5) * orientation + 0.5; 19369 } 19370 const firstPoint = new Point(points[0], points[1]); 19371 const lastPoint = new Point(points[points.length - 2], points[points.length - 1]); 19372 const closedShape = closed; 19373 const closedPath = Math.abs(firstPoint.x - lastPoint.x) < eps && Math.abs(firstPoint.y - lastPoint.y) < eps; 19374 if (closedShape) { 19375 points = points.slice(); 19376 if (closedPath) { 19377 points.pop(); 19378 points.pop(); 19379 lastPoint.set(points[points.length - 2], points[points.length - 1]); 19380 } 19381 const midPointX = (firstPoint.x + lastPoint.x) * 0.5; 19382 const midPointY = (lastPoint.y + firstPoint.y) * 0.5; 19383 points.unshift(midPointX, midPointY); 19384 points.push(midPointX, midPointY); 19385 } 19386 const verts = vertices; 19387 const length = points.length / 2; 19388 let indexCount = points.length; 19389 const indexStart = verts.length / 2; 19390 const width = style.width / 2; 19391 const widthSquared = width * width; 19392 const miterLimitSquared = style.miterLimit * style.miterLimit; 19393 let x0 = points[0]; 19394 let y0 = points[1]; 19395 let x1 = points[2]; 19396 let y1 = points[3]; 19397 let x2 = 0; 19398 let y2 = 0; 19399 let perpX = -(y0 - y1); 19400 let perpY = x0 - x1; 19401 let perp1x = 0; 19402 let perp1y = 0; 19403 let dist = Math.sqrt(perpX * perpX + perpY * perpY); 19404 perpX /= dist; 19405 perpY /= dist; 19406 perpX *= width; 19407 perpY *= width; 19408 const ratio = alignment; 19409 const innerWeight = (1 - ratio) * 2; 19410 const outerWeight = ratio * 2; 19411 if (!closedShape) { 19412 if (style.cap === "round") { 19413 indexCount += round( 19414 x0 - perpX * (innerWeight - outerWeight) * 0.5, 19415 y0 - perpY * (innerWeight - outerWeight) * 0.5, 19416 x0 - perpX * innerWeight, 19417 y0 - perpY * innerWeight, 19418 x0 + perpX * outerWeight, 19419 y0 + perpY * outerWeight, 19420 verts, 19421 true 19422 ) + 2; 19423 } else if (style.cap === "square") { 19424 indexCount += square(x0, y0, perpX, perpY, innerWeight, outerWeight, true, verts); 19425 } 19426 } 19427 verts.push( 19428 x0 - perpX * innerWeight, 19429 y0 - perpY * innerWeight 19430 ); 19431 verts.push( 19432 x0 + perpX * outerWeight, 19433 y0 + perpY * outerWeight 19434 ); 19435 for (let i = 1; i < length - 1; ++i) { 19436 x0 = points[(i - 1) * 2]; 19437 y0 = points[(i - 1) * 2 + 1]; 19438 x1 = points[i * 2]; 19439 y1 = points[i * 2 + 1]; 19440 x2 = points[(i + 1) * 2]; 19441 y2 = points[(i + 1) * 2 + 1]; 19442 perpX = -(y0 - y1); 19443 perpY = x0 - x1; 19444 dist = Math.sqrt(perpX * perpX + perpY * perpY); 19445 perpX /= dist; 19446 perpY /= dist; 19447 perpX *= width; 19448 perpY *= width; 19449 perp1x = -(y1 - y2); 19450 perp1y = x1 - x2; 19451 dist = Math.sqrt(perp1x * perp1x + perp1y * perp1y); 19452 perp1x /= dist; 19453 perp1y /= dist; 19454 perp1x *= width; 19455 perp1y *= width; 19456 const dx0 = x1 - x0; 19457 const dy0 = y0 - y1; 19458 const dx1 = x1 - x2; 19459 const dy1 = y2 - y1; 19460 const dot = dx0 * dx1 + dy0 * dy1; 19461 const cross = dy0 * dx1 - dy1 * dx0; 19462 const clockwise = cross < 0; 19463 if (Math.abs(cross) < 1e-3 * Math.abs(dot)) { 19464 verts.push( 19465 x1 - perpX * innerWeight, 19466 y1 - perpY * innerWeight 19467 ); 19468 verts.push( 19469 x1 + perpX * outerWeight, 19470 y1 + perpY * outerWeight 19471 ); 19472 if (dot >= 0) { 19473 if (style.join === "round") { 19474 indexCount += round( 19475 x1, 19476 y1, 19477 x1 - perpX * innerWeight, 19478 y1 - perpY * innerWeight, 19479 x1 - perp1x * innerWeight, 19480 y1 - perp1y * innerWeight, 19481 verts, 19482 false 19483 ) + 4; 19484 } else { 19485 indexCount += 2; 19486 } 19487 verts.push( 19488 x1 - perp1x * outerWeight, 19489 y1 - perp1y * outerWeight 19490 ); 19491 verts.push( 19492 x1 + perp1x * innerWeight, 19493 y1 + perp1y * innerWeight 19494 ); 19495 } 19496 continue; 19497 } 19498 const c1 = (-perpX + x0) * (-perpY + y1) - (-perpX + x1) * (-perpY + y0); 19499 const c2 = (-perp1x + x2) * (-perp1y + y1) - (-perp1x + x1) * (-perp1y + y2); 19500 const px = (dx0 * c2 - dx1 * c1) / cross; 19501 const py = (dy1 * c1 - dy0 * c2) / cross; 19502 const pDist = (px - x1) * (px - x1) + (py - y1) * (py - y1); 19503 const imx = x1 + (px - x1) * innerWeight; 19504 const imy = y1 + (py - y1) * innerWeight; 19505 const omx = x1 - (px - x1) * outerWeight; 19506 const omy = y1 - (py - y1) * outerWeight; 19507 const smallerInsideSegmentSq = Math.min(dx0 * dx0 + dy0 * dy0, dx1 * dx1 + dy1 * dy1); 19508 const insideWeight = clockwise ? innerWeight : outerWeight; 19509 const smallerInsideDiagonalSq = smallerInsideSegmentSq + insideWeight * insideWeight * widthSquared; 19510 const insideMiterOk = pDist <= smallerInsideDiagonalSq; 19511 if (insideMiterOk) { 19512 if (style.join === "bevel" || pDist / widthSquared > miterLimitSquared) { 19513 if (clockwise) { 19514 verts.push(imx, imy); 19515 verts.push(x1 + perpX * outerWeight, y1 + perpY * outerWeight); 19516 verts.push(imx, imy); 19517 verts.push(x1 + perp1x * outerWeight, y1 + perp1y * outerWeight); 19518 } else { 19519 verts.push(x1 - perpX * innerWeight, y1 - perpY * innerWeight); 19520 verts.push(omx, omy); 19521 verts.push(x1 - perp1x * innerWeight, y1 - perp1y * innerWeight); 19522 verts.push(omx, omy); 19523 } 19524 indexCount += 2; 19525 } else if (style.join === "round") { 19526 if (clockwise) { 19527 verts.push(imx, imy); 19528 verts.push(x1 + perpX * outerWeight, y1 + perpY * outerWeight); 19529 indexCount += round( 19530 x1, 19531 y1, 19532 x1 + perpX * outerWeight, 19533 y1 + perpY * outerWeight, 19534 x1 + perp1x * outerWeight, 19535 y1 + perp1y * outerWeight, 19536 verts, 19537 true 19538 ) + 4; 19539 verts.push(imx, imy); 19540 verts.push(x1 + perp1x * outerWeight, y1 + perp1y * outerWeight); 19541 } else { 19542 verts.push(x1 - perpX * innerWeight, y1 - perpY * innerWeight); 19543 verts.push(omx, omy); 19544 indexCount += round( 19545 x1, 19546 y1, 19547 x1 - perpX * innerWeight, 19548 y1 - perpY * innerWeight, 19549 x1 - perp1x * innerWeight, 19550 y1 - perp1y * innerWeight, 19551 verts, 19552 false 19553 ) + 4; 19554 verts.push(x1 - perp1x * innerWeight, y1 - perp1y * innerWeight); 19555 verts.push(omx, omy); 19556 } 19557 } else { 19558 verts.push(imx, imy); 19559 verts.push(omx, omy); 19560 } 19561 } else { 19562 verts.push(x1 - perpX * innerWeight, y1 - perpY * innerWeight); 19563 verts.push(x1 + perpX * outerWeight, y1 + perpY * outerWeight); 19564 if (style.join === "round") { 19565 if (clockwise) { 19566 indexCount += round( 19567 x1, 19568 y1, 19569 x1 + perpX * outerWeight, 19570 y1 + perpY * outerWeight, 19571 x1 + perp1x * outerWeight, 19572 y1 + perp1y * outerWeight, 19573 verts, 19574 true 19575 ) + 2; 19576 } else { 19577 indexCount += round( 19578 x1, 19579 y1, 19580 x1 - perpX * innerWeight, 19581 y1 - perpY * innerWeight, 19582 x1 - perp1x * innerWeight, 19583 y1 - perp1y * innerWeight, 19584 verts, 19585 false 19586 ) + 2; 19587 } 19588 } else if (style.join === "miter" && pDist / widthSquared <= miterLimitSquared) { 19589 if (clockwise) { 19590 verts.push(omx, omy); 19591 verts.push(omx, omy); 19592 } else { 19593 verts.push(imx, imy); 19594 verts.push(imx, imy); 19595 } 19596 indexCount += 2; 19597 } 19598 verts.push(x1 - perp1x * innerWeight, y1 - perp1y * innerWeight); 19599 verts.push(x1 + perp1x * outerWeight, y1 + perp1y * outerWeight); 19600 indexCount += 2; 19601 } 19602 } 19603 x0 = points[(length - 2) * 2]; 19604 y0 = points[(length - 2) * 2 + 1]; 19605 x1 = points[(length - 1) * 2]; 19606 y1 = points[(length - 1) * 2 + 1]; 19607 perpX = -(y0 - y1); 19608 perpY = x0 - x1; 19609 dist = Math.sqrt(perpX * perpX + perpY * perpY); 19610 perpX /= dist; 19611 perpY /= dist; 19612 perpX *= width; 19613 perpY *= width; 19614 verts.push(x1 - perpX * innerWeight, y1 - perpY * innerWeight); 19615 verts.push(x1 + perpX * outerWeight, y1 + perpY * outerWeight); 19616 if (!closedShape) { 19617 if (style.cap === "round") { 19618 indexCount += round( 19619 x1 - perpX * (innerWeight - outerWeight) * 0.5, 19620 y1 - perpY * (innerWeight - outerWeight) * 0.5, 19621 x1 - perpX * innerWeight, 19622 y1 - perpY * innerWeight, 19623 x1 + perpX * outerWeight, 19624 y1 + perpY * outerWeight, 19625 verts, 19626 false 19627 ) + 2; 19628 } else if (style.cap === "square") { 19629 indexCount += square(x1, y1, perpX, perpY, innerWeight, outerWeight, false, verts); 19630 } 19631 } 19632 const eps2 = curveEps * curveEps; 19633 for (let i = indexStart; i < indexCount + indexStart - 2; ++i) { 19634 x0 = verts[i * 2]; 19635 y0 = verts[i * 2 + 1]; 19636 x1 = verts[(i + 1) * 2]; 19637 y1 = verts[(i + 1) * 2 + 1]; 19638 x2 = verts[(i + 2) * 2]; 19639 y2 = verts[(i + 2) * 2 + 1]; 19640 if (Math.abs(x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1)) < eps2) { 19641 continue; 19642 } 19643 indices.push(i, i + 1, i + 2); 19644 } 19645} 19646 19647function buildPixelLine(points, closed, vertices, indices) { 19648 const eps = closePointEps; 19649 if (points.length === 0) { 19650 return; 19651 } 19652 const fx = points[0]; 19653 const fy = points[1]; 19654 const lx = points[points.length - 2]; 19655 const ly = points[points.length - 1]; 19656 const closePath = closed || Math.abs(fx - lx) < eps && Math.abs(fy - ly) < eps; 19657 const verts = vertices; 19658 const length = points.length / 2; 19659 const indexStart = verts.length / 2; 19660 for (let i = 0; i < length; i++) { 19661 verts.push(points[i * 2]); 19662 verts.push(points[i * 2 + 1]); 19663 } 19664 for (let i = 0; i < length - 1; i++) { 19665 indices.push(indexStart + i, indexStart + i + 1); 19666 } 19667 if (closePath) { 19668 indices.push(indexStart + length - 1, indexStart); 19669 } 19670} 19671 19672function triangulateWithHoles(points, holes, vertices, verticesStride, verticesOffset, indices, indicesOffset) { 19673 const triangles = earcut(points, holes, 2); 19674 if (!triangles) { 19675 return; 19676 } 19677 for (let i = 0; i < triangles.length; i += 3) { 19678 indices[indicesOffset++] = triangles[i] + verticesOffset; 19679 indices[indicesOffset++] = triangles[i + 1] + verticesOffset; 19680 indices[indicesOffset++] = triangles[i + 2] + verticesOffset; 19681 } 19682 let index = verticesOffset * verticesStride; 19683 for (let i = 0; i < points.length; i += 2) { 19684 vertices[index] = points[i]; 19685 vertices[index + 1] = points[i + 1]; 19686 index += verticesStride; 19687 } 19688} 19689 19690const emptyArray = []; 19691const buildPolygon = { 19692 extension: { 19693 type: ExtensionType.ShapeBuilder, 19694 name: "polygon" 19695 }, 19696 build(shape, points) { 19697 for (let i = 0; i < shape.points.length; i++) { 19698 points[i] = shape.points[i]; 19699 } 19700 return true; 19701 }, 19702 triangulate(points, vertices, verticesStride, verticesOffset, indices, indicesOffset) { 19703 triangulateWithHoles(points, emptyArray, vertices, verticesStride, verticesOffset, indices, indicesOffset); 19704 } 19705}; 19706 19707const buildRectangle = { 19708 extension: { 19709 type: ExtensionType.ShapeBuilder, 19710 name: "rectangle" 19711 }, 19712 build(shape, points) { 19713 const rectData = shape; 19714 const x = rectData.x; 19715 const y = rectData.y; 19716 const width = rectData.width; 19717 const height = rectData.height; 19718 if (!(width > 0 && height > 0)) { 19719 return false; 19720 } 19721 points[0] = x; 19722 points[1] = y; 19723 points[2] = x + width; 19724 points[3] = y; 19725 points[4] = x + width; 19726 points[5] = y + height; 19727 points[6] = x; 19728 points[7] = y + height; 19729 return true; 19730 }, 19731 triangulate(points, vertices, verticesStride, verticesOffset, indices, indicesOffset) { 19732 let count = 0; 19733 verticesOffset *= verticesStride; 19734 vertices[verticesOffset + count] = points[0]; 19735 vertices[verticesOffset + count + 1] = points[1]; 19736 count += verticesStride; 19737 vertices[verticesOffset + count] = points[2]; 19738 vertices[verticesOffset + count + 1] = points[3]; 19739 count += verticesStride; 19740 vertices[verticesOffset + count] = points[6]; 19741 vertices[verticesOffset + count + 1] = points[7]; 19742 count += verticesStride; 19743 vertices[verticesOffset + count] = points[4]; 19744 vertices[verticesOffset + count + 1] = points[5]; 19745 count += verticesStride; 19746 const verticesIndex = verticesOffset / verticesStride; 19747 indices[indicesOffset++] = verticesIndex; 19748 indices[indicesOffset++] = verticesIndex + 1; 19749 indices[indicesOffset++] = verticesIndex + 2; 19750 indices[indicesOffset++] = verticesIndex + 1; 19751 indices[indicesOffset++] = verticesIndex + 3; 19752 indices[indicesOffset++] = verticesIndex + 2; 19753 } 19754}; 19755 19756const buildTriangle = { 19757 extension: { 19758 type: ExtensionType.ShapeBuilder, 19759 name: "triangle" 19760 }, 19761 build(shape, points) { 19762 points[0] = shape.x; 19763 points[1] = shape.y; 19764 points[2] = shape.x2; 19765 points[3] = shape.y2; 19766 points[4] = shape.x3; 19767 points[5] = shape.y3; 19768 return true; 19769 }, 19770 triangulate(points, vertices, verticesStride, verticesOffset, indices, indicesOffset) { 19771 let count = 0; 19772 verticesOffset *= verticesStride; 19773 vertices[verticesOffset + count] = points[0]; 19774 vertices[verticesOffset + count + 1] = points[1]; 19775 count += verticesStride; 19776 vertices[verticesOffset + count] = points[2]; 19777 vertices[verticesOffset + count + 1] = points[3]; 19778 count += verticesStride; 19779 vertices[verticesOffset + count] = points[4]; 19780 vertices[verticesOffset + count + 1] = points[5]; 19781 const verticesIndex = verticesOffset / verticesStride; 19782 indices[indicesOffset++] = verticesIndex; 19783 indices[indicesOffset++] = verticesIndex + 1; 19784 indices[indicesOffset++] = verticesIndex + 2; 19785 } 19786}; 19787 19788const tempTextureMatrix$1 = new Matrix(); 19789const tempRect$1 = new Rectangle(); 19790function generateTextureMatrix(out, style, shape, matrix) { 19791 const textureMatrix = style.matrix ? out.copyFrom(style.matrix).invert() : out.identity(); 19792 if (style.textureSpace === "local") { 19793 const bounds = shape.getBounds(tempRect$1); 19794 if (style.width) { 19795 bounds.pad(style.width); 19796 } 19797 const { x: tx, y: ty } = bounds; 19798 const sx = 1 / bounds.width; 19799 const sy = 1 / bounds.height; 19800 const mTx = -tx * sx; 19801 const mTy = -ty * sy; 19802 const a1 = textureMatrix.a; 19803 const b1 = textureMatrix.b; 19804 const c1 = textureMatrix.c; 19805 const d1 = textureMatrix.d; 19806 textureMatrix.a *= sx; 19807 textureMatrix.b *= sx; 19808 textureMatrix.c *= sy; 19809 textureMatrix.d *= sy; 19810 textureMatrix.tx = mTx * a1 + mTy * c1 + textureMatrix.tx; 19811 textureMatrix.ty = mTx * b1 + mTy * d1 + textureMatrix.ty; 19812 } else { 19813 textureMatrix.translate(style.texture.frame.x, style.texture.frame.y); 19814 textureMatrix.scale(1 / style.texture.source.width, 1 / style.texture.source.height); 19815 } 19816 const sourceStyle = style.texture.source.style; 19817 if (!(style.fill instanceof FillGradient) && sourceStyle.addressMode === "clamp-to-edge") { 19818 sourceStyle.addressMode = "repeat"; 19819 sourceStyle.update(); 19820 } 19821 if (matrix) { 19822 textureMatrix.append(tempTextureMatrix$1.copyFrom(matrix).invert()); 19823 } 19824 return textureMatrix; 19825} 19826 19827const shapeBuilders = {}; 19828extensions.handleByMap(ExtensionType.ShapeBuilder, shapeBuilders); 19829extensions.add(buildRectangle, buildPolygon, buildTriangle, buildCircle, buildEllipse, buildRoundedRectangle); 19830const tempRect = new Rectangle(); 19831const tempTextureMatrix = new Matrix(); 19832function buildContextBatches(context, gpuContext) { 19833 const { geometryData, batches } = gpuContext; 19834 batches.length = 0; 19835 geometryData.indices.length = 0; 19836 geometryData.vertices.length = 0; 19837 geometryData.uvs.length = 0; 19838 for (let i = 0; i < context.instructions.length; i++) { 19839 const instruction = context.instructions[i]; 19840 if (instruction.action === "texture") { 19841 addTextureToGeometryData(instruction.data, batches, geometryData); 19842 } else if (instruction.action === "fill" || instruction.action === "stroke") { 19843 const isStroke = instruction.action === "stroke"; 19844 const shapePath = instruction.data.path.shapePath; 19845 const style = instruction.data.style; 19846 const hole = instruction.data.hole; 19847 if (isStroke && hole) { 19848 addShapePathToGeometryData(hole.shapePath, style, true, batches, geometryData); 19849 } 19850 if (hole) { 19851 shapePath.shapePrimitives[shapePath.shapePrimitives.length - 1].holes = hole.shapePath.shapePrimitives; 19852 } 19853 addShapePathToGeometryData(shapePath, style, isStroke, batches, geometryData); 19854 } 19855 } 19856} 19857function addTextureToGeometryData(data, batches, geometryData) { 19858 const points = []; 19859 const build = shapeBuilders.rectangle; 19860 const rect = tempRect; 19861 rect.x = data.dx; 19862 rect.y = data.dy; 19863 rect.width = data.dw; 19864 rect.height = data.dh; 19865 const matrix = data.transform; 19866 if (!build.build(rect, points)) { 19867 return; 19868 } 19869 const { vertices, uvs, indices } = geometryData; 19870 const indexOffset = indices.length; 19871 const vertOffset = vertices.length / 2; 19872 if (matrix) { 19873 transformVertices(points, matrix); 19874 } 19875 build.triangulate(points, vertices, 2, vertOffset, indices, indexOffset); 19876 const texture = data.image; 19877 const textureUvs = texture.uvs; 19878 uvs.push( 19879 textureUvs.x0, 19880 textureUvs.y0, 19881 textureUvs.x1, 19882 textureUvs.y1, 19883 textureUvs.x3, 19884 textureUvs.y3, 19885 textureUvs.x2, 19886 textureUvs.y2 19887 ); 19888 const graphicsBatch = BigPool.get(BatchableGraphics); 19889 graphicsBatch.indexOffset = indexOffset; 19890 graphicsBatch.indexSize = indices.length - indexOffset; 19891 graphicsBatch.attributeOffset = vertOffset; 19892 graphicsBatch.attributeSize = vertices.length / 2 - vertOffset; 19893 graphicsBatch.baseColor = data.style; 19894 graphicsBatch.alpha = data.alpha; 19895 graphicsBatch.texture = texture; 19896 graphicsBatch.geometryData = geometryData; 19897 batches.push(graphicsBatch); 19898} 19899function addShapePathToGeometryData(shapePath, style, isStroke, batches, geometryData) { 19900 const { vertices, uvs, indices } = geometryData; 19901 shapePath.shapePrimitives.forEach(({ shape, transform: matrix, holes }) => { 19902 const points = []; 19903 const build = shapeBuilders[shape.type]; 19904 if (!build.build(shape, points)) { 19905 return; 19906 } 19907 const indexOffset = indices.length; 19908 const vertOffset = vertices.length / 2; 19909 let topology = "triangle-list"; 19910 if (matrix) { 19911 transformVertices(points, matrix); 19912 } 19913 if (!isStroke) { 19914 if (holes) { 19915 const holeIndices = []; 19916 const otherPoints = points.slice(); 19917 const holeArrays = getHoleArrays(holes); 19918 holeArrays.forEach((holePoints) => { 19919 holeIndices.push(otherPoints.length / 2); 19920 otherPoints.push(...holePoints); 19921 }); 19922 triangulateWithHoles(otherPoints, holeIndices, vertices, 2, vertOffset, indices, indexOffset); 19923 } else { 19924 build.triangulate(points, vertices, 2, vertOffset, indices, indexOffset); 19925 } 19926 } else { 19927 const close = shape.closePath ?? true; 19928 const lineStyle = style; 19929 if (!lineStyle.pixelLine) { 19930 buildLine(points, lineStyle, false, close, vertices, indices); 19931 } else { 19932 buildPixelLine(points, close, vertices, indices); 19933 topology = "line-list"; 19934 } 19935 } 19936 const uvsOffset = uvs.length / 2; 19937 const texture = style.texture; 19938 if (texture !== Texture.WHITE) { 19939 const textureMatrix = generateTextureMatrix(tempTextureMatrix, style, shape, matrix); 19940 buildUvs(vertices, 2, vertOffset, uvs, uvsOffset, 2, vertices.length / 2 - vertOffset, textureMatrix); 19941 } else { 19942 buildSimpleUvs(uvs, uvsOffset, 2, vertices.length / 2 - vertOffset); 19943 } 19944 const graphicsBatch = BigPool.get(BatchableGraphics); 19945 graphicsBatch.indexOffset = indexOffset; 19946 graphicsBatch.indexSize = indices.length - indexOffset; 19947 graphicsBatch.attributeOffset = vertOffset; 19948 graphicsBatch.attributeSize = vertices.length / 2 - vertOffset; 19949 graphicsBatch.baseColor = style.color; 19950 graphicsBatch.alpha = style.alpha; 19951 graphicsBatch.texture = texture; 19952 graphicsBatch.geometryData = geometryData; 19953 graphicsBatch.topology = topology; 19954 batches.push(graphicsBatch); 19955 }); 19956} 19957function getHoleArrays(holePrimitives) { 19958 const holeArrays = []; 19959 for (let k = 0; k < holePrimitives.length; k++) { 19960 const holePrimitive = holePrimitives[k].shape; 19961 const holePoints = []; 19962 const holeBuilder = shapeBuilders[holePrimitive.type]; 19963 if (holeBuilder.build(holePrimitive, holePoints)) { 19964 holeArrays.push(holePoints); 19965 } 19966 } 19967 return holeArrays; 19968} 19969 19970class GpuGraphicsContext { 19971 constructor() { 19972 this.batches = []; 19973 this.geometryData = { 19974 vertices: [], 19975 uvs: [], 19976 indices: [] 19977 }; 19978 } 19979 reset() { 19980 if (this.batches) { 19981 this.batches.forEach((batch) => { 19982 BigPool.return(batch); 19983 }); 19984 } 19985 if (this.graphicsData) { 19986 BigPool.return(this.graphicsData); 19987 } 19988 this.isBatchable = false; 19989 this.context = null; 19990 this.batches.length = 0; 19991 this.geometryData.indices.length = 0; 19992 this.geometryData.vertices.length = 0; 19993 this.geometryData.uvs.length = 0; 19994 this.graphicsData = null; 19995 } 19996 destroy() { 19997 this.reset(); 19998 this.batches = null; 19999 this.geometryData = null; 20000 } 20001} 20002class GraphicsContextRenderData { 20003 constructor() { 20004 this.instructions = new InstructionSet(); 20005 } 20006 init(options) { 20007 const maxTextures = options.maxTextures; 20008 this.batcher ? this.batcher._updateMaxTextures(maxTextures) : this.batcher = new DefaultBatcher({ maxTextures }); 20009 this.instructions.reset(); 20010 } 20011 /** 20012 * @deprecated since version 8.0.0 20013 * Use `batcher.geometry` instead. 20014 * @see {Batcher#geometry} 20015 */ 20016 get geometry() { 20017 deprecation(v8_3_4, "GraphicsContextRenderData#geometry is deprecated, please use batcher.geometry instead."); 20018 return this.batcher.geometry; 20019 } 20020 destroy() { 20021 this.batcher.destroy(); 20022 this.instructions.destroy(); 20023 this.batcher = null; 20024 this.instructions = null; 20025 } 20026} 20027const _GraphicsContextSystem = class _GraphicsContextSystem { 20028 constructor(renderer) { 20029 this._renderer = renderer; 20030 this._managedContexts = new GCManagedHash({ renderer, type: "resource", name: "graphicsContext" }); 20031 } 20032 /** 20033 * Runner init called, update the default options 20034 * @ignore 20035 */ 20036 init(options) { 20037 _GraphicsContextSystem.defaultOptions.bezierSmoothness = options?.bezierSmoothness ?? _GraphicsContextSystem.defaultOptions.bezierSmoothness; 20038 } 20039 /** 20040 * Returns the render data for a given GraphicsContext. 20041 * @param context - The GraphicsContext to get the render data for. 20042 * @internal 20043 */ 20044 getContextRenderData(context) { 20045 return context._gpuData[this._renderer.uid].graphicsData || this._initContextRenderData(context); 20046 } 20047 /** 20048 * Updates the GPU context for a given GraphicsContext. 20049 * If the context is dirty, it will rebuild the batches and geometry data. 20050 * @param context - The GraphicsContext to update. 20051 * @returns The updated GpuGraphicsContext. 20052 * @internal 20053 */ 20054 updateGpuContext(context) { 20055 const hasContext = !!context._gpuData[this._renderer.uid]; 20056 const gpuContext = context._gpuData[this._renderer.uid] || this._initContext(context); 20057 if (context.dirty || !hasContext) { 20058 if (hasContext) { 20059 gpuContext.reset(); 20060 } 20061 buildContextBatches(context, gpuContext); 20062 const batchMode = context.batchMode; 20063 if (context.customShader || batchMode === "no-batch") { 20064 gpuContext.isBatchable = false; 20065 } else if (batchMode === "auto") { 20066 gpuContext.isBatchable = gpuContext.geometryData.vertices.length < 400; 20067 } else { 20068 gpuContext.isBatchable = true; 20069 } 20070 context.dirty = false; 20071 } 20072 return gpuContext; 20073 } 20074 /** 20075 * Returns the GpuGraphicsContext for a given GraphicsContext. 20076 * If it does not exist, it will initialize a new one. 20077 * @param context - The GraphicsContext to get the GpuGraphicsContext for. 20078 * @returns The GpuGraphicsContext for the given GraphicsContext. 20079 * @internal 20080 */ 20081 getGpuContext(context) { 20082 return context._gpuData[this._renderer.uid] || this._initContext(context); 20083 } 20084 _initContextRenderData(context) { 20085 const graphicsData = BigPool.get(GraphicsContextRenderData, { 20086 maxTextures: this._renderer.limits.maxBatchableTextures 20087 }); 20088 const gpuContext = context._gpuData[this._renderer.uid]; 20089 const { batches, geometryData } = gpuContext; 20090 gpuContext.graphicsData = graphicsData; 20091 const vertexSize = geometryData.vertices.length; 20092 const indexSize = geometryData.indices.length; 20093 for (let i = 0; i < batches.length; i++) { 20094 batches[i].applyTransform = false; 20095 } 20096 const batcher = graphicsData.batcher; 20097 batcher.ensureAttributeBuffer(vertexSize); 20098 batcher.ensureIndexBuffer(indexSize); 20099 batcher.begin(); 20100 for (let i = 0; i < batches.length; i++) { 20101 const batch = batches[i]; 20102 batcher.add(batch); 20103 } 20104 batcher.finish(graphicsData.instructions); 20105 const geometry = batcher.geometry; 20106 geometry.indexBuffer.setDataWithSize(batcher.indexBuffer, batcher.indexSize, true); 20107 geometry.buffers[0].setDataWithSize(batcher.attributeBuffer.float32View, batcher.attributeSize, true); 20108 const drawBatches = batcher.batches; 20109 for (let i = 0; i < drawBatches.length; i++) { 20110 const batch = drawBatches[i]; 20111 batch.bindGroup = getTextureBatchBindGroup( 20112 batch.textures.textures, 20113 batch.textures.count, 20114 this._renderer.limits.maxBatchableTextures 20115 ); 20116 } 20117 return graphicsData; 20118 } 20119 _initContext(context) { 20120 const gpuContext = new GpuGraphicsContext(); 20121 gpuContext.context = context; 20122 context._gpuData[this._renderer.uid] = gpuContext; 20123 this._managedContexts.add(context); 20124 return gpuContext; 20125 } 20126 destroy() { 20127 this._managedContexts.destroy(); 20128 this._renderer = null; 20129 } 20130}; 20131/** @ignore */ 20132_GraphicsContextSystem.extension = { 20133 type: [ 20134 ExtensionType.WebGLSystem, 20135 ExtensionType.WebGPUSystem, 20136 ExtensionType.CanvasSystem 20137 ], 20138 name: "graphicsContext" 20139}; 20140/** The default options for the GraphicsContextSystem. */ 20141_GraphicsContextSystem.defaultOptions = { 20142 /** 20143 * A value from 0 to 1 that controls the smoothness of bezier curves (the higher the smoother) 20144 * @default 0.5 20145 */ 20146 bezierSmoothness: 0.5 20147}; 20148let GraphicsContextSystem = _GraphicsContextSystem; 20149 20150const RECURSION_LIMIT$1 = 8; 20151const FLT_EPSILON$1 = 11920929e-14; 20152const PATH_DISTANCE_EPSILON$1 = 1; 20153function buildAdaptiveBezier(points, sX, sY, cp1x, cp1y, cp2x, cp2y, eX, eY, smoothness) { 20154 const scale = 1; 20155 const smoothing = Math.min( 20156 0.99, 20157 // a value of 1.0 actually inverts smoothing, so we cap it at 0.99 20158 Math.max(0, smoothness ?? GraphicsContextSystem.defaultOptions.bezierSmoothness) 20159 ); 20160 let distanceTolerance = (PATH_DISTANCE_EPSILON$1 - smoothing) / scale; 20161 distanceTolerance *= distanceTolerance; 20162 begin$1(sX, sY, cp1x, cp1y, cp2x, cp2y, eX, eY, points, distanceTolerance); 20163 return points; 20164} 20165function begin$1(sX, sY, cp1x, cp1y, cp2x, cp2y, eX, eY, points, distanceTolerance) { 20166 recursive$1(sX, sY, cp1x, cp1y, cp2x, cp2y, eX, eY, points, distanceTolerance, 0); 20167 points.push(eX, eY); 20168} 20169function recursive$1(x1, y1, x2, y2, x3, y3, x4, y4, points, distanceTolerance, level) { 20170 if (level > RECURSION_LIMIT$1) { 20171 return; 20172 } 20173 const x12 = (x1 + x2) / 2; 20174 const y12 = (y1 + y2) / 2; 20175 const x23 = (x2 + x3) / 2; 20176 const y23 = (y2 + y3) / 2; 20177 const x34 = (x3 + x4) / 2; 20178 const y34 = (y3 + y4) / 2; 20179 const x123 = (x12 + x23) / 2; 20180 const y123 = (y12 + y23) / 2; 20181 const x234 = (x23 + x34) / 2; 20182 const y234 = (y23 + y34) / 2; 20183 const x1234 = (x123 + x234) / 2; 20184 const y1234 = (y123 + y234) / 2; 20185 if (level > 0) { 20186 let dx = x4 - x1; 20187 let dy = y4 - y1; 20188 const d2 = Math.abs((x2 - x4) * dy - (y2 - y4) * dx); 20189 const d3 = Math.abs((x3 - x4) * dy - (y3 - y4) * dx); 20190 if (d2 > FLT_EPSILON$1 && d3 > FLT_EPSILON$1) { 20191 if ((d2 + d3) * (d2 + d3) <= distanceTolerance * (dx * dx + dy * dy)) { 20192 { 20193 points.push(x1234, y1234); 20194 return; 20195 } 20196 } 20197 } else if (d2 > FLT_EPSILON$1) { 20198 if (d2 * d2 <= distanceTolerance * (dx * dx + dy * dy)) { 20199 { 20200 points.push(x1234, y1234); 20201 return; 20202 } 20203 } 20204 } else if (d3 > FLT_EPSILON$1) { 20205 if (d3 * d3 <= distanceTolerance * (dx * dx + dy * dy)) { 20206 { 20207 points.push(x1234, y1234); 20208 return; 20209 } 20210 } 20211 } else { 20212 dx = x1234 - (x1 + x4) / 2; 20213 dy = y1234 - (y1 + y4) / 2; 20214 if (dx * dx + dy * dy <= distanceTolerance) { 20215 points.push(x1234, y1234); 20216 return; 20217 } 20218 } 20219 } 20220 recursive$1(x1, y1, x12, y12, x123, y123, x1234, y1234, points, distanceTolerance, level + 1); 20221 recursive$1(x1234, y1234, x234, y234, x34, y34, x4, y4, points, distanceTolerance, level + 1); 20222} 20223 20224const RECURSION_LIMIT = 8; 20225const FLT_EPSILON = 11920929e-14; 20226const PATH_DISTANCE_EPSILON = 1; 20227function buildAdaptiveQuadratic(points, sX, sY, cp1x, cp1y, eX, eY, smoothness) { 20228 const scale = 1; 20229 const smoothing = Math.min( 20230 0.99, 20231 // a value of 1.0 actually inverts smoothing, so we cap it at 0.99 20232 Math.max(0, smoothness ?? GraphicsContextSystem.defaultOptions.bezierSmoothness) 20233 ); 20234 let distanceTolerance = (PATH_DISTANCE_EPSILON - smoothing) / scale; 20235 distanceTolerance *= distanceTolerance; 20236 begin(sX, sY, cp1x, cp1y, eX, eY, points, distanceTolerance); 20237 return points; 20238} 20239function begin(sX, sY, cp1x, cp1y, eX, eY, points, distanceTolerance) { 20240 recursive(points, sX, sY, cp1x, cp1y, eX, eY, distanceTolerance, 0); 20241 points.push(eX, eY); 20242} 20243function recursive(points, x1, y1, x2, y2, x3, y3, distanceTolerance, level) { 20244 if (level > RECURSION_LIMIT) { 20245 return; 20246 } 20247 const x12 = (x1 + x2) / 2; 20248 const y12 = (y1 + y2) / 2; 20249 const x23 = (x2 + x3) / 2; 20250 const y23 = (y2 + y3) / 2; 20251 const x123 = (x12 + x23) / 2; 20252 const y123 = (y12 + y23) / 2; 20253 let dx = x3 - x1; 20254 let dy = y3 - y1; 20255 const d = Math.abs((x2 - x3) * dy - (y2 - y3) * dx); 20256 if (d > FLT_EPSILON) { 20257 if (d * d <= distanceTolerance * (dx * dx + dy * dy)) { 20258 { 20259 points.push(x123, y123); 20260 return; 20261 } 20262 } 20263 } else { 20264 dx = x123 - (x1 + x3) / 2; 20265 dy = y123 - (y1 + y3) / 2; 20266 if (dx * dx + dy * dy <= distanceTolerance) { 20267 points.push(x123, y123); 20268 return; 20269 } 20270 } 20271 recursive(points, x1, y1, x12, y12, x123, y123, distanceTolerance, level + 1); 20272 recursive(points, x123, y123, x23, y23, x3, y3, distanceTolerance, level + 1); 20273} 20274 20275function buildArc(points, x, y, radius, start, end, clockwise, steps) { 20276 let dist = Math.abs(start - end); 20277 if (!clockwise && start > end) { 20278 dist = 2 * Math.PI - dist; 20279 } else if (clockwise && end > start) { 20280 dist = 2 * Math.PI - dist; 20281 } 20282 steps || (steps = Math.max(6, Math.floor(6 * Math.pow(radius, 1 / 3) * (dist / Math.PI)))); 20283 steps = Math.max(steps, 3); 20284 let f = dist / steps; 20285 let t = start; 20286 f *= clockwise ? -1 : 1; 20287 for (let i = 0; i < steps + 1; i++) { 20288 const cs = Math.cos(t); 20289 const sn = Math.sin(t); 20290 const nx = x + cs * radius; 20291 const ny = y + sn * radius; 20292 points.push(nx, ny); 20293 t += f; 20294 } 20295} 20296 20297function buildArcTo(points, x1, y1, x2, y2, radius) { 20298 const fromX = points[points.length - 2]; 20299 const fromY = points[points.length - 1]; 20300 const a1 = fromY - y1; 20301 const b1 = fromX - x1; 20302 const a2 = y2 - y1; 20303 const b2 = x2 - x1; 20304 const mm = Math.abs(a1 * b2 - b1 * a2); 20305 if (mm < 1e-8 || radius === 0) { 20306 if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) { 20307 points.push(x1, y1); 20308 } 20309 return; 20310 } 20311 const dd = a1 * a1 + b1 * b1; 20312 const cc = a2 * a2 + b2 * b2; 20313 const tt = a1 * a2 + b1 * b2; 20314 const k1 = radius * Math.sqrt(dd) / mm; 20315 const k2 = radius * Math.sqrt(cc) / mm; 20316 const j1 = k1 * tt / dd; 20317 const j2 = k2 * tt / cc; 20318 const cx = k1 * b2 + k2 * b1; 20319 const cy = k1 * a2 + k2 * a1; 20320 const px = b1 * (k2 + j1); 20321 const py = a1 * (k2 + j1); 20322 const qx = b2 * (k1 + j2); 20323 const qy = a2 * (k1 + j2); 20324 const startAngle = Math.atan2(py - cy, px - cx); 20325 const endAngle = Math.atan2(qy - cy, qx - cx); 20326 buildArc( 20327 points, 20328 cx + x1, 20329 cy + y1, 20330 radius, 20331 startAngle, 20332 endAngle, 20333 b1 * a2 > b2 * a1 20334 ); 20335} 20336 20337const TAU = Math.PI * 2; 20338const out = { 20339 centerX: 0, 20340 centerY: 0, 20341 ang1: 0, 20342 ang2: 0 20343}; 20344const mapToEllipse = ({ x, y }, rx, ry, cosPhi, sinPhi, centerX, centerY, out2) => { 20345 x *= rx; 20346 y *= ry; 20347 const xp = cosPhi * x - sinPhi * y; 20348 const yp = sinPhi * x + cosPhi * y; 20349 out2.x = xp + centerX; 20350 out2.y = yp + centerY; 20351 return out2; 20352}; 20353function approxUnitArc(ang1, ang2) { 20354 const a1 = ang2 === -1.5707963267948966 ? -0.551915024494 : 4 / 3 * Math.tan(ang2 / 4); 20355 const a = ang2 === 1.5707963267948966 ? 0.551915024494 : a1; 20356 const x1 = Math.cos(ang1); 20357 const y1 = Math.sin(ang1); 20358 const x2 = Math.cos(ang1 + ang2); 20359 const y2 = Math.sin(ang1 + ang2); 20360 return [ 20361 { 20362 x: x1 - y1 * a, 20363 y: y1 + x1 * a 20364 }, 20365 { 20366 x: x2 + y2 * a, 20367 y: y2 - x2 * a 20368 }, 20369 { 20370 x: x2, 20371 y: y2 20372 } 20373 ]; 20374} 20375const vectorAngle = (ux, uy, vx, vy) => { 20376 const sign = ux * vy - uy * vx < 0 ? -1 : 1; 20377 let dot = ux * vx + uy * vy; 20378 if (dot > 1) { 20379 dot = 1; 20380 } 20381 if (dot < -1) { 20382 dot = -1; 20383 } 20384 return sign * Math.acos(dot); 20385}; 20386const getArcCenter = (px, py, cx, cy, rx, ry, largeArcFlag, sweepFlag, sinPhi, cosPhi, pxp, pyp, out2) => { 20387 const rxSq = Math.pow(rx, 2); 20388 const rySq = Math.pow(ry, 2); 20389 const pxpSq = Math.pow(pxp, 2); 20390 const pypSq = Math.pow(pyp, 2); 20391 let radicant = rxSq * rySq - rxSq * pypSq - rySq * pxpSq; 20392 if (radicant < 0) { 20393 radicant = 0; 20394 } 20395 radicant /= rxSq * pypSq + rySq * pxpSq; 20396 radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1); 20397 const centerXp = radicant * rx / ry * pyp; 20398 const centerYp = radicant * -ry / rx * pxp; 20399 const centerX = cosPhi * centerXp - sinPhi * centerYp + (px + cx) / 2; 20400 const centerY = sinPhi * centerXp + cosPhi * centerYp + (py + cy) / 2; 20401 const vx1 = (pxp - centerXp) / rx; 20402 const vy1 = (pyp - centerYp) / ry; 20403 const vx2 = (-pxp - centerXp) / rx; 20404 const vy2 = (-pyp - centerYp) / ry; 20405 const ang1 = vectorAngle(1, 0, vx1, vy1); 20406 let ang2 = vectorAngle(vx1, vy1, vx2, vy2); 20407 if (sweepFlag === 0 && ang2 > 0) { 20408 ang2 -= TAU; 20409 } 20410 if (sweepFlag === 1 && ang2 < 0) { 20411 ang2 += TAU; 20412 } 20413 out2.centerX = centerX; 20414 out2.centerY = centerY; 20415 out2.ang1 = ang1; 20416 out2.ang2 = ang2; 20417}; 20418function buildArcToSvg(points, px, py, cx, cy, rx, ry, xAxisRotation = 0, largeArcFlag = 0, sweepFlag = 0) { 20419 if (rx === 0 || ry === 0) { 20420 return; 20421 } 20422 const sinPhi = Math.sin(xAxisRotation * TAU / 360); 20423 const cosPhi = Math.cos(xAxisRotation * TAU / 360); 20424 const pxp = cosPhi * (px - cx) / 2 + sinPhi * (py - cy) / 2; 20425 const pyp = -sinPhi * (px - cx) / 2 + cosPhi * (py - cy) / 2; 20426 if (pxp === 0 && pyp === 0) { 20427 return; 20428 } 20429 rx = Math.abs(rx); 20430 ry = Math.abs(ry); 20431 const lambda = Math.pow(pxp, 2) / Math.pow(rx, 2) + Math.pow(pyp, 2) / Math.pow(ry, 2); 20432 if (lambda > 1) { 20433 rx *= Math.sqrt(lambda); 20434 ry *= Math.sqrt(lambda); 20435 } 20436 getArcCenter( 20437 px, 20438 py, 20439 cx, 20440 cy, 20441 rx, 20442 ry, 20443 largeArcFlag, 20444 sweepFlag, 20445 sinPhi, 20446 cosPhi, 20447 pxp, 20448 pyp, 20449 out 20450 ); 20451 let { ang1, ang2 } = out; 20452 const { centerX, centerY } = out; 20453 let ratio = Math.abs(ang2) / (TAU / 4); 20454 if (Math.abs(1 - ratio) < 1e-7) { 20455 ratio = 1; 20456 } 20457 const segments = Math.max(Math.ceil(ratio), 1); 20458 ang2 /= segments; 20459 let lastX = points[points.length - 2]; 20460 let lastY = points[points.length - 1]; 20461 const outCurvePoint = { x: 0, y: 0 }; 20462 for (let i = 0; i < segments; i++) { 20463 const curve = approxUnitArc(ang1, ang2); 20464 const { x: x1, y: y1 } = mapToEllipse(curve[0], rx, ry, cosPhi, sinPhi, centerX, centerY, outCurvePoint); 20465 const { x: x2, y: y2 } = mapToEllipse(curve[1], rx, ry, cosPhi, sinPhi, centerX, centerY, outCurvePoint); 20466 const { x, y } = mapToEllipse(curve[2], rx, ry, cosPhi, sinPhi, centerX, centerY, outCurvePoint); 20467 buildAdaptiveBezier( 20468 points, 20469 lastX, 20470 lastY, 20471 x1, 20472 y1, 20473 x2, 20474 y2, 20475 x, 20476 y 20477 ); 20478 lastX = x; 20479 lastY = y; 20480 ang1 += ang2; 20481 } 20482} 20483 20484function roundedShapeArc(g, points, radius) { 20485 const vecFrom = (p, pp) => { 20486 const x = pp.x - p.x; 20487 const y = pp.y - p.y; 20488 const len = Math.sqrt(x * x + y * y); 20489 const nx = x / len; 20490 const ny = y / len; 20491 return { len, nx, ny }; 20492 }; 20493 const sharpCorner = (i, p) => { 20494 if (i === 0) { 20495 g.moveTo(p.x, p.y); 20496 } else { 20497 g.lineTo(p.x, p.y); 20498 } 20499 }; 20500 let p1 = points[points.length - 1]; 20501 for (let i = 0; i < points.length; i++) { 20502 const p2 = points[i % points.length]; 20503 const pRadius = p2.radius ?? radius; 20504 if (pRadius <= 0) { 20505 sharpCorner(i, p2); 20506 p1 = p2; 20507 continue; 20508 } 20509 const p3 = points[(i + 1) % points.length]; 20510 const v1 = vecFrom(p2, p1); 20511 const v2 = vecFrom(p2, p3); 20512 if (v1.len < 1e-4 || v2.len < 1e-4) { 20513 sharpCorner(i, p2); 20514 p1 = p2; 20515 continue; 20516 } 20517 let angle = Math.asin(v1.nx * v2.ny - v1.ny * v2.nx); 20518 let radDirection = 1; 20519 let drawDirection = false; 20520 if (v1.nx * v2.nx - v1.ny * -v2.ny < 0) { 20521 if (angle < 0) { 20522 angle = Math.PI + angle; 20523 } else { 20524 angle = Math.PI - angle; 20525 radDirection = -1; 20526 drawDirection = true; 20527 } 20528 } else if (angle > 0) { 20529 radDirection = -1; 20530 drawDirection = true; 20531 } 20532 const halfAngle = angle / 2; 20533 let cRadius; 20534 let lenOut = Math.abs( 20535 Math.cos(halfAngle) * pRadius / Math.sin(halfAngle) 20536 ); 20537 if (lenOut > Math.min(v1.len / 2, v2.len / 2)) { 20538 lenOut = Math.min(v1.len / 2, v2.len / 2); 20539 cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle)); 20540 } else { 20541 cRadius = pRadius; 20542 } 20543 const cX = p2.x + v2.nx * lenOut + -v2.ny * cRadius * radDirection; 20544 const cY = p2.y + v2.ny * lenOut + v2.nx * cRadius * radDirection; 20545 const startAngle = Math.atan2(v1.ny, v1.nx) + Math.PI / 2 * radDirection; 20546 const endAngle = Math.atan2(v2.ny, v2.nx) - Math.PI / 2 * radDirection; 20547 if (i === 0) { 20548 g.moveTo( 20549 cX + Math.cos(startAngle) * cRadius, 20550 cY + Math.sin(startAngle) * cRadius 20551 ); 20552 } 20553 g.arc(cX, cY, cRadius, startAngle, endAngle, drawDirection); 20554 p1 = p2; 20555 } 20556} 20557function roundedShapeQuadraticCurve(g, points, radius, smoothness) { 20558 const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2); 20559 const pointLerp = (p1, p2, t) => ({ 20560 x: p1.x + (p2.x - p1.x) * t, 20561 y: p1.y + (p2.y - p1.y) * t 20562 }); 20563 const numPoints = points.length; 20564 for (let i = 0; i < numPoints; i++) { 20565 const thisPoint = points[(i + 1) % numPoints]; 20566 const pRadius = thisPoint.radius ?? radius; 20567 if (pRadius <= 0) { 20568 if (i === 0) { 20569 g.moveTo(thisPoint.x, thisPoint.y); 20570 } else { 20571 g.lineTo(thisPoint.x, thisPoint.y); 20572 } 20573 continue; 20574 } 20575 const lastPoint = points[i]; 20576 const nextPoint = points[(i + 2) % numPoints]; 20577 const lastEdgeLength = distance(lastPoint, thisPoint); 20578 let start; 20579 if (lastEdgeLength < 1e-4) { 20580 start = thisPoint; 20581 } else { 20582 const lastOffsetDistance = Math.min(lastEdgeLength / 2, pRadius); 20583 start = pointLerp( 20584 thisPoint, 20585 lastPoint, 20586 lastOffsetDistance / lastEdgeLength 20587 ); 20588 } 20589 const nextEdgeLength = distance(nextPoint, thisPoint); 20590 let end; 20591 if (nextEdgeLength < 1e-4) { 20592 end = thisPoint; 20593 } else { 20594 const nextOffsetDistance = Math.min(nextEdgeLength / 2, pRadius); 20595 end = pointLerp( 20596 thisPoint, 20597 nextPoint, 20598 nextOffsetDistance / nextEdgeLength 20599 ); 20600 } 20601 if (i === 0) { 20602 g.moveTo(start.x, start.y); 20603 } else { 20604 g.lineTo(start.x, start.y); 20605 } 20606 g.quadraticCurveTo(thisPoint.x, thisPoint.y, end.x, end.y, smoothness); 20607 } 20608} 20609 20610const tempRectangle = new Rectangle(); 20611class ShapePath { 20612 constructor(graphicsPath2D) { 20613 /** The list of shape primitives that make up the path. */ 20614 this.shapePrimitives = []; 20615 this._currentPoly = null; 20616 this._bounds = new Bounds(); 20617 this._graphicsPath2D = graphicsPath2D; 20618 this.signed = graphicsPath2D.checkForHoles; 20619 } 20620 /** 20621 * Sets the starting point for a new sub-path. Any subsequent drawing commands are considered part of this path. 20622 * @param x - The x-coordinate for the starting point. 20623 * @param y - The y-coordinate for the starting point. 20624 * @returns The instance of the current object for chaining. 20625 */ 20626 moveTo(x, y) { 20627 this.startPoly(x, y); 20628 return this; 20629 } 20630 /** 20631 * Connects the current point to a new point with a straight line. This method updates the current path. 20632 * @param x - The x-coordinate of the new point to connect to. 20633 * @param y - The y-coordinate of the new point to connect to. 20634 * @returns The instance of the current object for chaining. 20635 */ 20636 lineTo(x, y) { 20637 this._ensurePoly(); 20638 const points = this._currentPoly.points; 20639 const fromX = points[points.length - 2]; 20640 const fromY = points[points.length - 1]; 20641 if (fromX !== x || fromY !== y) { 20642 points.push(x, y); 20643 } 20644 return this; 20645 } 20646 /** 20647 * Adds an arc to the path. The arc is centered at (x, y) 20648 * position with radius `radius` starting at `startAngle` and ending at `endAngle`. 20649 * @param x - The x-coordinate of the arc's center. 20650 * @param y - The y-coordinate of the arc's center. 20651 * @param radius - The radius of the arc. 20652 * @param startAngle - The starting angle of the arc, in radians. 20653 * @param endAngle - The ending angle of the arc, in radians.
20654 * @param counterclockwise - Specifies whether the arc should be drawn in the anticlockwise direction. False by default. 20655 * @returns The instance of the current object for chaining. 20656 */ 20657 arc(x, y, radius, startAngle, endAngle, counterclockwise) { 20658 this._ensurePoly(false); 20659 const points = this._currentPoly.points; 20660 buildArc(points, x, y, radius, startAngle, endAngle, counterclockwise); 20661 return this; 20662 } 20663 /** 20664 * Adds an arc to the path with the arc tangent to the line joining two specified points. 20665 * The arc radius is specified by `radius`. 20666 * @param x1 - The x-coordinate of the first point. 20667 * @param y1 - The y-coordinate of the first point. 20668 * @param x2 - The x-coordinate of the second point. 20669 * @param y2 - The y-coordinate of the second point. 20670 * @param radius - The radius of the arc. 20671 * @returns The instance of the current object for chaining. 20672 */ 20673 arcTo(x1, y1, x2, y2, radius) { 20674 this._ensurePoly(); 20675 const points = this._currentPoly.points; 20676 buildArcTo(points, x1, y1, x2, y2, radius); 20677 return this; 20678 } 20679 /** 20680 * Adds an SVG-style arc to the path, allowing for elliptical arcs based on the SVG spec. 20681 * @param rx - The x-radius of the ellipse. 20682 * @param ry - The y-radius of the ellipse. 20683 * @param xAxisRotation - The rotation of the ellipse's x-axis relative 20684 * to the x-axis of the coordinate system, in degrees. 20685 * @param largeArcFlag - Determines if the arc should be greater than or less than 180 degrees. 20686 * @param sweepFlag - Determines if the arc should be swept in a positive angle direction. 20687 * @param x - The x-coordinate of the arc's end point. 20688 * @param y - The y-coordinate of the arc's end point. 20689 * @returns The instance of the current object for chaining. 20690 */ 20691 arcToSvg(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y) { 20692 const points = this._currentPoly.points; 20693 buildArcToSvg( 20694 points, 20695 this._currentPoly.lastX, 20696 this._currentPoly.lastY, 20697 x, 20698 y, 20699 rx, 20700 ry, 20701 xAxisRotation, 20702 largeArcFlag, 20703 sweepFlag 20704 ); 20705 return this; 20706 } 20707 /** 20708 * Adds a cubic Bezier curve to the path. 20709 * It requires three points: the first two are control points and the third one is the end point. 20710 * The starting point is the last point in the current path. 20711 * @param cp1x - The x-coordinate of the first control point. 20712 * @param cp1y - The y-coordinate of the first control point. 20713 * @param cp2x - The x-coordinate of the second control point. 20714 * @param cp2y - The y-coordinate of the second control point. 20715 * @param x - The x-coordinate of the end point. 20716 * @param y - The y-coordinate of the end point. 20717 * @param smoothness - Optional parameter to adjust the smoothness of the curve. 20718 * @returns The instance of the current object for chaining. 20719 */ 20720 bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y, smoothness) { 20721 this._ensurePoly(); 20722 const currentPoly = this._currentPoly; 20723 buildAdaptiveBezier( 20724 this._currentPoly.points, 20725 currentPoly.lastX, 20726 currentPoly.lastY, 20727 cp1x, 20728 cp1y, 20729 cp2x, 20730 cp2y, 20731 x, 20732 y, 20733 smoothness 20734 ); 20735 return this; 20736 } 20737 /** 20738 * Adds a quadratic curve to the path. It requires two points: the control point and the end point. 20739 * The starting point is the last point in the current path. 20740 * @param cp1x - The x-coordinate of the control point. 20741 * @param cp1y - The y-coordinate of the control point. 20742 * @param x - The x-coordinate of the end point. 20743 * @param y - The y-coordinate of the end point. 20744 * @param smoothing - Optional parameter to adjust the smoothness of the curve. 20745 * @returns The instance of the current object for chaining. 20746 */ 20747 quadraticCurveTo(cp1x, cp1y, x, y, smoothing) { 20748 this._ensurePoly(); 20749 const currentPoly = this._currentPoly; 20750 buildAdaptiveQuadratic( 20751 this._currentPoly.points, 20752 currentPoly.lastX, 20753 currentPoly.lastY, 20754 cp1x, 20755 cp1y, 20756 x, 20757 y, 20758 smoothing 20759 ); 20760 return this; 20761 } 20762 /** 20763 * Closes the current path by drawing a straight line back to the start. 20764 * If the shape is already closed or there are no points in the path, this method does nothing. 20765 * @returns The instance of the current object for chaining. 20766 */ 20767 closePath() { 20768 this.endPoly(true); 20769 return this; 20770 } 20771 /** 20772 * Adds another path to the current path. This method allows for the combination of multiple paths into one. 20773 * @param path - The `GraphicsPath` object representing the path to add. 20774 * @param transform - An optional `Matrix` object to apply a transformation to the path before adding it. 20775 * @returns The instance of the current object for chaining. 20776 */ 20777 addPath(path, transform) { 20778 this.endPoly(); 20779 if (transform && !transform.isIdentity()) { 20780 path = path.clone(true); 20781 path.transform(transform); 20782 } 20783 const shapePrimitives = this.shapePrimitives; 20784 const start = shapePrimitives.length; 20785 for (let i = 0; i < path.instructions.length; i++) { 20786 const instruction = path.instructions[i]; 20787 this[instruction.action](...instruction.data); 20788 } 20789 if (path.checkForHoles && shapePrimitives.length - start > 1) { 20790 let mainShape = null; 20791 for (let i = start; i < shapePrimitives.length; i++) { 20792 const shapePrimitive = shapePrimitives[i]; 20793 if (shapePrimitive.shape.type === "polygon") { 20794 const polygon = shapePrimitive.shape; 20795 const mainPolygon = mainShape?
vendor: 12,518 bytes, lines 20795-21087
20795.shape; 20796 if (mainPolygon && mainPolygon.containsPolygon(polygon)) { 20797 mainShape.holes || (mainShape.holes = []); 20798 mainShape.holes.push(shapePrimitive); 20799 shapePrimitives.copyWithin(i, i + 1); 20800 shapePrimitives.length--; 20801 i--; 20802 } else { 20803 mainShape = shapePrimitive; 20804 } 20805 } 20806 } 20807 } 20808 return this; 20809 } 20810 /** 20811 * Finalizes the drawing of the current path. Optionally, it can close the path. 20812 * @param closePath - A boolean indicating whether to close the path after finishing. False by default. 20813 */ 20814 finish(closePath = false) { 20815 this.endPoly(closePath); 20816 } 20817 /** 20818 * Draws a rectangle shape. This method adds a new rectangle path to the current drawing. 20819 * @param x - The x-coordinate of the top-left corner of the rectangle. 20820 * @param y - The y-coordinate of the top-left corner of the rectangle. 20821 * @param w - The width of the rectangle. 20822 * @param h - The height of the rectangle. 20823 * @param transform - An optional `Matrix` object to apply a transformation to the rectangle. 20824 * @returns The instance of the current object for chaining. 20825 */ 20826 rect(x, y, w, h, transform) { 20827 this.drawShape(new Rectangle(x, y, w, h), transform); 20828 return this; 20829 } 20830 /** 20831 * Draws a circle shape. This method adds a new circle path to the current drawing. 20832 * @param x - The x-coordinate of the center of the circle. 20833 * @param y - The y-coordinate of the center of the circle. 20834 * @param radius - The radius of the circle. 20835 * @param transform - An optional `Matrix` object to apply a transformation to the circle. 20836 * @returns The instance of the current object for chaining. 20837 */ 20838 circle(x, y, radius, transform) { 20839 this.drawShape(new Circle(x, y, radius), transform); 20840 return this; 20841 } 20842 /** 20843 * Draws a polygon shape. This method allows for the creation of complex polygons by specifying a sequence of points. 20844 * @param points - An array of numbers, or or an array of PointData objects eg [{x,y}, {x,y}, {x,y}] 20845 * representing the x and y coordinates of the polygon's vertices, in sequence. 20846 * @param close - A boolean indicating whether to close the polygon path. True by default. 20847 * @param transform - An optional `Matrix` object to apply a transformation to the polygon. 20848 * @returns The instance of the current object for chaining. 20849 */ 20850 poly(points, close, transform) { 20851 const polygon = new Polygon(points); 20852 polygon.closePath = close; 20853 this.drawShape(polygon, transform); 20854 return this; 20855 } 20856 /** 20857 * Draws a regular polygon with a specified number of sides. All sides and angles are equal. 20858 * @param x - The x-coordinate of the center of the polygon. 20859 * @param y - The y-coordinate of the center of the polygon. 20860 * @param radius - The radius of the circumscribed circle of the polygon. 20861 * @param sides - The number of sides of the polygon. Must be 3 or more. 20862 * @param rotation - The rotation angle of the polygon, in radians. Zero by default. 20863 * @param transform - An optional `Matrix` object to apply a transformation to the polygon. 20864 * @returns The instance of the current object for chaining. 20865 */ 20866 regularPoly(x, y, radius, sides, rotation = 0, transform) { 20867 sides = Math.max(sides | 0, 3); 20868 const startAngle = -1 * Math.PI / 2 + rotation; 20869 const delta = Math.PI * 2 / sides; 20870 const polygon = []; 20871 for (let i = 0; i < sides; i++) { 20872 const angle = startAngle - i * delta; 20873 polygon.push( 20874 x + radius * Math.cos(angle), 20875 y + radius * Math.sin(angle) 20876 ); 20877 } 20878 this.poly(polygon, true, transform); 20879 return this; 20880 } 20881 /** 20882 * Draws a polygon with rounded corners. 20883 * Similar to `regularPoly` but with the ability to round the corners of the polygon. 20884 * @param x - The x-coordinate of the center of the polygon. 20885 * @param y - The y-coordinate of the center of the polygon. 20886 * @param radius - The radius of the circumscribed circle of the polygon. 20887 * @param sides - The number of sides of the polygon. Must be 3 or more. 20888 * @param corner - The radius of the rounding of the corners. 20889 * @param rotation - The rotation angle of the polygon, in radians. Zero by default. 20890 * @param smoothness - Optional parameter to adjust the smoothness of the rounding. 20891 * @returns The instance of the current object for chaining. 20892 */ 20893 roundPoly(x, y, radius, sides, corner, rotation = 0, smoothness) { 20894 sides = Math.max(sides | 0, 3); 20895 if (corner <= 0) { 20896 return this.regularPoly(x, y, radius, sides, rotation); 20897 } 20898 const sideLength = radius * Math.sin(Math.PI / sides) - 1e-3; 20899 corner = Math.min(corner, sideLength); 20900 const startAngle = -1 * Math.PI / 2 + rotation; 20901 const delta = Math.PI * 2 / sides; 20902 const internalAngle = (sides - 2) * Math.PI / sides / 2; 20903 for (let i = 0; i < sides; i++) { 20904 const angle = i * delta + startAngle; 20905 const x0 = x + radius * Math.cos(angle); 20906 const y0 = y + radius * Math.sin(angle); 20907 const a1 = angle + Math.PI + internalAngle; 20908 const a2 = angle - Math.PI - internalAngle; 20909 const x1 = x0 + corner * Math.cos(a1); 20910 const y1 = y0 + corner * Math.sin(a1); 20911 const x3 = x0 + corner * Math.cos(a2); 20912 const y3 = y0 + corner * Math.sin(a2); 20913 if (i === 0) { 20914 this.moveTo(x1, y1); 20915 } else { 20916 this.lineTo(x1, y1); 20917 } 20918 this.quadraticCurveTo(x0, y0, x3, y3, smoothness); 20919 } 20920 return this.closePath(); 20921 } 20922 /** 20923 * Draws a shape with rounded corners. This function supports custom radius for each corner of the shape. 20924 * Optionally, corners can be rounded using a quadratic curve instead of an arc, providing a different aesthetic. 20925 * @param points - An array of `RoundedPoint` representing the corners of the shape to draw. 20926 * A minimum of 3 points is required. 20927 * @param radius - The default radius for the corners. 20928 * This radius is applied to all corners unless overridden in `points`. 20929 * @param useQuadratic - If set to true, rounded corners are drawn using a quadraticCurve 20930 * method instead of an arc method. Defaults to false. 20931 * @param smoothness - Specifies the smoothness of the curve when `useQuadratic` is true. 20932 * Higher values make the curve smoother. 20933 * @returns The instance of the current object for chaining. 20934 */ 20935 roundShape(points, radius, useQuadratic = false, smoothness) { 20936 if (points.length < 3) { 20937 return this; 20938 } 20939 if (useQuadratic) { 20940 roundedShapeQuadraticCurve(this, points, radius, smoothness); 20941 } else { 20942 roundedShapeArc(this, points, radius); 20943 } 20944 return this.closePath(); 20945 } 20946 /** 20947 * Draw Rectangle with fillet corners. This is much like rounded rectangle 20948 * however it support negative numbers as well for the corner radius. 20949 * @param x - Upper left corner of rect 20950 * @param y - Upper right corner of rect 20951 * @param width - Width of rect 20952 * @param height - Height of rect 20953 * @param fillet - accept negative or positive values 20954 */ 20955 filletRect(x, y, width, height, fillet) { 20956 if (fillet === 0) { 20957 return this.rect(x, y, width, height); 20958 } 20959 const maxFillet = Math.min(width, height) / 2; 20960 const inset = Math.min(maxFillet, Math.max(-maxFillet, fillet)); 20961 const right = x + width; 20962 const bottom = y + height; 20963 const dir = inset < 0 ? -inset : 0; 20964 const size = Math.abs(inset); 20965 return this.moveTo(x, y + size).arcTo(x + dir, y + dir, x + size, y, size).lineTo(right - size, y).arcTo(right - dir, y + dir, right, y + size, size).lineTo(right, bottom - size).arcTo(right - dir, bottom - dir, x + width - size, bottom, size).lineTo(x + size, bottom).arcTo(x + dir, bottom - dir, x, bottom - size, size).closePath(); 20966 } 20967 /** 20968 * Draw Rectangle with chamfer corners. These are angled corners. 20969 * @param x - Upper left corner of rect 20970 * @param y - Upper right corner of rect 20971 * @param width - Width of rect 20972 * @param height - Height of rect 20973 * @param chamfer - non-zero real number, size of corner cutout 20974 * @param transform 20975 */ 20976 chamferRect(x, y, width, height, chamfer, transform) { 20977 if (chamfer <= 0) { 20978 return this.rect(x, y, width, height); 20979 } 20980 const inset = Math.min(chamfer, Math.min(width, height) / 2); 20981 const right = x + width; 20982 const bottom = y + height; 20983 const points = [ 20984 x + inset, 20985 y, 20986 right - inset, 20987 y, 20988 right, 20989 y + inset, 20990 right, 20991 bottom - inset, 20992 right - inset, 20993 bottom, 20994 x + inset, 20995 bottom, 20996 x, 20997 bottom - inset, 20998 x, 20999 y + inset 21000 ]; 21001 for (let i = points.length - 1; i >= 2; i -= 2) { 21002 if (points[i] === points[i - 2] && points[i - 1] === points[i - 3]) { 21003 points.splice(i - 1, 2); 21004 } 21005 } 21006 return this.poly(points, true, transform); 21007 } 21008 /** 21009 * Draws an ellipse at the specified location and with the given x and y radii. 21010 * An optional transformation can be applied, allowing for rotation, scaling, and translation. 21011 * @param x - The x-coordinate of the center of the ellipse. 21012 * @param y - The y-coordinate of the center of the ellipse. 21013 * @param radiusX - The horizontal radius of the ellipse. 21014 * @param radiusY - The vertical radius of the ellipse. 21015 * @param transform - An optional `Matrix` object to apply a transformation to the ellipse. This can include rotations. 21016 * @returns The instance of the current object for chaining. 21017 */ 21018 ellipse(x, y, radiusX, radiusY, transform) { 21019 this.drawShape(new Ellipse(x, y, radiusX, radiusY), transform); 21020 return this; 21021 } 21022 /** 21023 * Draws a rectangle with rounded corners. 21024 * The corner radius can be specified to determine how rounded the corners should be. 21025 * An optional transformation can be applied, which allows for rotation, scaling, and translation of the rectangle. 21026 * @param x - The x-coordinate of the top-left corner of the rectangle. 21027 * @param y - The y-coordinate of the top-left corner of the rectangle. 21028 * @param w - The width of the rectangle. 21029 * @param h - The height of the rectangle. 21030 * @param radius - The radius of the rectangle's corners. If not specified, corners will be sharp. 21031 * @param transform - An optional `Matrix` object to apply a transformation to the rectangle. 21032 * @returns The instance of the current object for chaining. 21033 */ 21034 roundRect(x, y, w, h, radius, transform) { 21035 this.drawShape(new RoundedRectangle(x, y, w, h, radius), transform); 21036 return this; 21037 } 21038 /** 21039 * Draws a given shape on the canvas. 21040 * This is a generic method that can draw any type of shape specified by the `ShapePrimitive` parameter. 21041 * An optional transformation matrix can be applied to the shape, allowing for complex transformations. 21042 * @param shape - The shape to draw, defined as a `ShapePrimitive` object. 21043 * @param matrix - An optional `Matrix` for transforming the shape. This can include rotations, 21044 * scaling, and translations. 21045 * @returns The instance of the current object for chaining. 21046 */ 21047 drawShape(shape, matrix) { 21048 this.endPoly(); 21049 this.shapePrimitives.push({ shape, transform: matrix }); 21050 return this; 21051 } 21052 /** 21053 * Starts a new polygon path from the specified starting point. 21054 * This method initializes a new polygon or ends the current one if it exists. 21055 * @param x - The x-coordinate of the starting point of the new polygon. 21056 * @param y - The y-coordinate of the starting point of the new polygon. 21057 * @returns The instance of the current object for chaining. 21058 */ 21059 startPoly(x, y) { 21060 let currentPoly = this._currentPoly; 21061 if (currentPoly) { 21062 this.endPoly(); 21063 } 21064 currentPoly = new Polygon(); 21065 currentPoly.points.push(x, y); 21066 this._currentPoly = currentPoly; 21067 return this; 21068 } 21069 /** 21070 * Ends the current polygon path. If `closePath` is set to true, 21071 * the path is closed by connecting the last point to the first one. 21072 * This method finalizes the current polygon and prepares it for drawing or adding to the shape primitives. 21073 * @param closePath - A boolean indicating whether to close the polygon by connecting the last point 21074 * back to the starting point. False by default. 21075 * @returns The instance of the current object for chaining. 21076 */ 21077 endPoly(closePath = false) { 21078 const shape = this._currentPoly; 21079 if (shape && shape.points.length > 2) { 21080 shape.closePath = closePath; 21081 this.shapePrimitives.push({ shape }); 21082 } 21083 this._currentPoly = null; 21084 return this; 21085 } 21086 _ensurePoly(start = true) { 21087 if (this._currentPoly)
21087 21088 return; 21089 this._currentPoly = new Polygon(); 21090 if (start) { 21091 const lastShape = this.shapePrimitives[this.shapePrimitives.length - 1]; 21092 if (lastShape) { 21093 let lx = lastShape.shape.x; 21094 let ly = lastShape.shape.y; 21095 if (lastShape.transform && !lastShape.transform.isIdentity()) { 21096 const t = lastShape.transform; 21097 const tempX = lx; 21098 lx = t.a * lx + t.c * ly + t.tx; 21099 ly = t.b * tempX + t.d * ly + t.ty; 21100 } 21101 this._currentPoly.points.push(lx, ly); 21102 } else { 21103 this._currentPoly.points.push(0, 0); 21104 } 21105 } 21106 } 21107 /** Builds the path. */ 21108 buildPath() { 21109 const path = this._graphicsPath2D; 21110 this.shapePrimitives.length = 0; 21111 this._currentPoly = null; 21112 for (let i = 0; i < path.instructions.length; i++) { 21113 const instruction = path.instructions[i]; 21114 this[instruction.action](...instruction.data); 21115 } 21116 this.finish(); 21117 } 21118 /** Gets the bounds of the path. */ 21119 get bounds() { 21120 const bounds = this._bounds; 21121 bounds.clear(); 21122 const shapePrimitives = this.shapePrimitives; 21123 for (let i = 0; i < shapePrimitives.length; i++) { 21124 const shapePrimitive = shapePrimitives[i]; 21125 const boundsRect = shapePrimitive.shape.getBounds(tempRectangle); 21126 if (shapePrimitive.transform) { 21127 bounds.addRect(boundsRect, shapePrimitive.transform); 21128 } else { 21129 bounds.addRect(boundsRect); 21130 } 21131 } 21132 return bounds; 21133 } 21134} 21135 21136class GraphicsPath { 21137 /** 21138 * Creates a `GraphicsPath` instance optionally from an SVG path string or an array of `PathInstruction`. 21139 * @param instructions - An SVG path string or an array of `PathInstruction` objects. 21140 * @param signed 21141 */ 21142 constructor(instructions, signed = false) { 21143 this.instructions = []; 21144 /** unique id for this graphics path */ 21145 this.uid = uid("graphicsPath"); 21146 this._dirty = true; 21147 this.checkForHoles = signed; 21148 if (typeof instructions === "string") { 21149 parseSVGPath(instructions, this); 21150 } else { 21151 this.instructions = instructions?.slice() ??
vendor: 10,832 bytes, lines 21151-21426
21151 []; 21152 } 21153 } 21154 /** 21155 * Provides access to the internal shape path, ensuring it is up-to-date with the current instructions. 21156 * @returns The `ShapePath` instance associated with this `GraphicsPath`. 21157 */ 21158 get shapePath() { 21159 if (!this._shapePath) { 21160 this._shapePath = new ShapePath(this); 21161 } 21162 if (this._dirty) { 21163 this._dirty = false; 21164 this._shapePath.buildPath(); 21165 } 21166 return this._shapePath; 21167 } 21168 /** 21169 * Adds another `GraphicsPath` to this path, optionally applying a transformation. 21170 * @param path - The `GraphicsPath` to add. 21171 * @param transform - An optional transformation to apply to the added path. 21172 * @returns The instance of the current object for chaining. 21173 */ 21174 addPath(path, transform) { 21175 path = path.clone(); 21176 this.instructions.push({ action: "addPath", data: [path, transform] }); 21177 this._dirty = true; 21178 return this; 21179 } 21180 arc(...args) { 21181 this.instructions.push({ action: "arc", data: args }); 21182 this._dirty = true; 21183 return this; 21184 } 21185 arcTo(...args) { 21186 this.instructions.push({ action: "arcTo", data: args }); 21187 this._dirty = true; 21188 return this; 21189 } 21190 arcToSvg(...args) { 21191 this.instructions.push({ action: "arcToSvg", data: args }); 21192 this._dirty = true; 21193 return this; 21194 } 21195 bezierCurveTo(...args) { 21196 this.instructions.push({ action: "bezierCurveTo", data: args }); 21197 this._dirty = true; 21198 return this; 21199 } 21200 /** 21201 * Adds a cubic Bezier curve to the path. 21202 * It requires two points: the second control point and the end point. The first control point is assumed to be 21203 * The starting point is the last point in the current path. 21204 * @param cp2x - The x-coordinate of the second control point. 21205 * @param cp2y - The y-coordinate of the second control point. 21206 * @param x - The x-coordinate of the end point. 21207 * @param y - The y-coordinate of the end point. 21208 * @param smoothness - Optional parameter to adjust the smoothness of the curve. 21209 * @returns The instance of the current object for chaining. 21210 */ 21211 bezierCurveToShort(cp2x, cp2y, x, y, smoothness) { 21212 const last = this.instructions[this.instructions.length - 1]; 21213 const lastPoint = this.getLastPoint(Point.shared); 21214 let cp1x = 0; 21215 let cp1y = 0; 21216 if (!last || last.action !== "bezierCurveTo") { 21217 cp1x = lastPoint.x; 21218 cp1y = lastPoint.y; 21219 } else { 21220 cp1x = last.data[2]; 21221 cp1y = last.data[3]; 21222 const currentX = lastPoint.x; 21223 const currentY = lastPoint.y; 21224 cp1x = currentX + (currentX - cp1x); 21225 cp1y = currentY + (currentY - cp1y); 21226 } 21227 this.instructions.push({ action: "bezierCurveTo", data: [cp1x, cp1y, cp2x, cp2y, x, y, smoothness] }); 21228 this._dirty = true; 21229 return this; 21230 } 21231 /** 21232 * Closes the current path by drawing a straight line back to the start. 21233 * If the shape is already closed or there are no points in the path, this method does nothing. 21234 * @returns The instance of the current object for chaining. 21235 */ 21236 closePath() { 21237 this.instructions.push({ action: "closePath", data: [] }); 21238 this._dirty = true; 21239 return this; 21240 } 21241 ellipse(...args) { 21242 this.instructions.push({ action: "ellipse", data: args }); 21243 this._dirty = true; 21244 return this; 21245 } 21246 lineTo(...args) { 21247 this.instructions.push({ action: "lineTo", data: args }); 21248 this._dirty = true; 21249 return this; 21250 } 21251 moveTo(...args) { 21252 this.instructions.push({ action: "moveTo", data: args }); 21253 return this; 21254 } 21255 quadraticCurveTo(...args) { 21256 this.instructions.push({ action: "quadraticCurveTo", data: args }); 21257 this._dirty = true; 21258 return this; 21259 } 21260 /** 21261 * Adds a quadratic curve to the path. It uses the previous point as the control point. 21262 * @param x - The x-coordinate of the end point. 21263 * @param y - The y-coordinate of the end point. 21264 * @param smoothness - Optional parameter to adjust the smoothness of the curve. 21265 * @returns The instance of the current object for chaining. 21266 */ 21267 quadraticCurveToShort(x, y, smoothness) { 21268 const last = this.instructions[this.instructions.length - 1]; 21269 const lastPoint = this.getLastPoint(Point.shared); 21270 let cpx1 = 0; 21271 let cpy1 = 0; 21272 if (!last || last.action !== "quadraticCurveTo") { 21273 cpx1 = lastPoint.x; 21274 cpy1 = lastPoint.y; 21275 } else { 21276 cpx1 = last.data[0]; 21277 cpy1 = last.data[1]; 21278 const currentX = lastPoint.x; 21279 const currentY = lastPoint.y; 21280 cpx1 = currentX + (currentX - cpx1); 21281 cpy1 = currentY + (currentY - cpy1); 21282 } 21283 this.instructions.push({ action: "quadraticCurveTo", data: [cpx1, cpy1, x, y, smoothness] }); 21284 this._dirty = true; 21285 return this; 21286 } 21287 /** 21288 * Draws a rectangle shape. This method adds a new rectangle path to the current drawing. 21289 * @param x - The x-coordinate of the top-left corner of the rectangle. 21290 * @param y - The y-coordinate of the top-left corner of the rectangle. 21291 * @param w - The width of the rectangle. 21292 * @param h - The height of the rectangle. 21293 * @param transform - An optional `Matrix` object to apply a transformation to the rectangle. 21294 * @returns The instance of the current object for chaining. 21295 */ 21296 rect(x, y, w, h, transform) { 21297 this.instructions.push({ action: "rect", data: [x, y, w, h, transform] }); 21298 this._dirty = true; 21299 return this; 21300 } 21301 /** 21302 * Draws a circle shape. This method adds a new circle path to the current drawing. 21303 * @param x - The x-coordinate of the center of the circle. 21304 * @param y - The y-coordinate of the center of the circle. 21305 * @param radius - The radius of the circle. 21306 * @param transform - An optional `Matrix` object to apply a transformation to the circle. 21307 * @returns The instance of the current object for chaining. 21308 */ 21309 circle(x, y, radius, transform) { 21310 this.instructions.push({ action: "circle", data: [x, y, radius, transform] }); 21311 this._dirty = true; 21312 return this; 21313 } 21314 roundRect(...args) { 21315 this.instructions.push({ action: "roundRect", data: args }); 21316 this._dirty = true; 21317 return this; 21318 } 21319 poly(...args) { 21320 this.instructions.push({ action: "poly", data: args }); 21321 this._dirty = true; 21322 return this; 21323 } 21324 regularPoly(...args) { 21325 this.instructions.push({ action: "regularPoly", data: args }); 21326 this._dirty = true; 21327 return this; 21328 } 21329 roundPoly(...args) { 21330 this.instructions.push({ action: "roundPoly", data: args }); 21331 this._dirty = true; 21332 return this; 21333 } 21334 roundShape(...args) { 21335 this.instructions.push({ action: "roundShape", data: args }); 21336 this._dirty = true; 21337 return this; 21338 } 21339 filletRect(...args) { 21340 this.instructions.push({ action: "filletRect", data: args }); 21341 this._dirty = true; 21342 return this; 21343 } 21344 chamferRect(...args) { 21345 this.instructions.push({ action: "chamferRect", data: args }); 21346 this._dirty = true; 21347 return this; 21348 } 21349 /** 21350 * Draws a star shape centered at a specified location. This method allows for the creation 21351 * of stars with a variable number of points, outer radius, optional inner radius, and rotation. 21352 * The star is drawn as a closed polygon with alternating outer and inner vertices to create the star's points. 21353 * An optional transformation can be applied to scale, rotate, or translate the star as needed. 21354 * @param x - The x-coordinate of the center of the star. 21355 * @param y - The y-coordinate of the center of the star. 21356 * @param points - The number of points of the star. 21357 * @param radius - The outer radius of the star (distance from the center to the outer points). 21358 * @param innerRadius - Optional. The inner radius of the star 21359 * (distance from the center to the inner points between the outer points). 21360 * If not provided, defaults to half of the `radius`. 21361 * @param rotation - Optional. The rotation of the star in radians, where 0 is aligned with the y-axis. 21362 * Defaults to 0, meaning one point is directly upward. 21363 * @param transform - An optional `Matrix` object to apply a transformation to the star. 21364 * This can include rotations, scaling, and translations. 21365 * @returns The instance of the current object for chaining further drawing commands. 21366 */ 21367 // eslint-disable-next-line max-len 21368 star(x, y, points, radius, innerRadius, rotation, transform) { 21369 innerRadius || (innerRadius = radius / 2); 21370 const startAngle = -1 * Math.PI / 2 + rotation; 21371 const len = points * 2; 21372 const delta = Math.PI * 2 / len; 21373 const polygon = []; 21374 for (let i = 0; i < len; i++) { 21375 const r = i % 2 ? innerRadius : radius; 21376 const angle = i * delta + startAngle; 21377 polygon.push( 21378 x + r * Math.cos(angle), 21379 y + r * Math.sin(angle) 21380 ); 21381 } 21382 this.poly(polygon, true, transform); 21383 return this; 21384 } 21385 /** 21386 * Creates a copy of the current `GraphicsPath` instance. This method supports both shallow and deep cloning. 21387 * A shallow clone copies the reference of the instructions array, while a deep clone creates a new array and 21388 * copies each instruction individually, ensuring that modifications to the instructions of the cloned `GraphicsPath` 21389 * do not affect the original `GraphicsPath` and vice versa. 21390 * @param deep - A boolean flag indicating whether the clone should be deep. 21391 * @returns A new `GraphicsPath` instance that is a clone of the current instance. 21392 */ 21393 clone(deep = false) { 21394 const newGraphicsPath2D = new GraphicsPath(); 21395 newGraphicsPath2D.checkForHoles = this.checkForHoles; 21396 if (!deep) { 21397 newGraphicsPath2D.instructions = this.instructions.slice(); 21398 } else { 21399 for (let i = 0; i < this.instructions.length; i++) { 21400 const instruction = this.instructions[i]; 21401 newGraphicsPath2D.instructions.push({ action: instruction.action, data: instruction.data.slice() }); 21402 } 21403 } 21404 return newGraphicsPath2D; 21405 } 21406 clear() { 21407 this.instructions.length = 0; 21408 this._dirty = true; 21409 return this; 21410 } 21411 /** 21412 * Applies a transformation matrix to all drawing instructions within the `GraphicsPath`. 21413 * This method enables the modification of the path's geometry according to the provided 21414 * transformation matrix, which can include translations, rotations, scaling, and skewing. 21415 * 21416 * Each drawing instruction in the path is updated to reflect the transformation, 21417 * ensuring the visual representation of the path is consistent with the applied matrix. 21418 * 21419 * Note: The transformation is applied directly to the coordinates and control points of the drawing instructions, 21420 * not to the path as a whole. This means the transformation's effects are baked into the individual instructions, 21421 * allowing for fine-grained control over the path's appearance. 21422 * @param matrix - A `Matrix` object representing the transformation to apply. 21423 * @returns The instance of the current object for chaining further operations. 21424 */ 21425 transform(matrix) { 21426 if (matrix.isIdentity())
21426 21427 return this; 21428 const a = matrix.a; 21429 const b = matrix.b; 21430 const c = matrix.c; 21431 const d = matrix.d; 21432 const tx = matrix.tx; 21433 const ty = matrix.ty; 21434 let x = 0; 21435 let y = 0;
21436 let cpx1 = 0; 21437 let cpy1 = 0; 21438 let cpx2 = 0; 21439 let cpy2 = 0; 21440 let rx = 0; 21441 let ry = 0; 21442 for (let i = 0; i < this.instructions.length; i++) { 21443 const instruction = this.instructions[i]; 21444 const data = instruction.data; 21445 switch (instruction.action) { 21446 case "moveTo": 21447 case "lineTo": 21448 x = data[0]; 21449 y = data[1]; 21450 data[0] = a * x + c * y + tx; 21451 data[1] = b * x + d * y + ty; 21452 break; 21453 case "bezierCurveTo": 21454 cpx1 = data[0]; 21455 cpy1 = data[1]; 21456 cpx2 = data[2]; 21457 cpy2 = data[3]; 21458 x = data[4]; 21459 y = data[5]; 21460 data[0] = a * cpx1 + c * cpy1 + tx; 21461 data[1] = b * cpx1 + d * cpy1 + ty; 21462 data[2] = a * cpx2 + c * cpy2 + tx; 21463 data[3] = b * cpx2 + d * cpy2 + ty; 21464 data[4] = a * x + c * y + tx; 21465 data[5] = b * x + d * y + ty; 21466 break; 21467 case "quadraticCurveTo": 21468 cpx1 = data[0]; 21469 cpy1 = data[1]; 21470 x = data[2]; 21471 y = data[3]; 21472 data[0] = a * cpx1 + c * cpy1 + tx; 21473 data[1] = b * cpx1 + d * cpy1 + ty; 21474 data[2] = a * x + c * y + tx; 21475 data[3] = b * x + d * y + ty; 21476 break; 21477 case "arcToSvg": 21478 x = data[5]; 21479 y = data[6]; 21480 rx = data[0]; 21481 ry = data[1]; 21482 data[0] = a * rx + c * ry; 21483 data[1] = b * rx + d * ry; 21484 data[5] = a * x + c * y + tx; 21485 data[6] = b * x + d * y + ty; 21486 break; 21487 case "circle": 21488 data[4] = adjustTransform(data[3], matrix); 21489 break; 21490 case "rect": 21491 data[4] = adjustTransform(data[4], matrix); 21492 break; 21493 case "ellipse": 21494 data[8] = adjustTransform(data[8], matrix); 21495 break; 21496 case "roundRect": 21497 data[5] = adjustTransform(data[5], matrix); 21498 break; 21499 case "addPath": 21500 data[0].transform(matrix); 21501 break; 21502 case "poly": 21503 data[2] = adjustTransform(data[2], matrix); 21504 break; 21505 default: 21506 warn("unknown transform action", instruction.action); 21507 break; 21508 } 21509 } 21510 this._dirty = true; 21511 return this; 21512 } 21513 get bounds() { 21514 return this.shapePath.bounds; 21515 } 21516 /** 21517 * Retrieves the last point from the current drawing instructions in the `GraphicsPath`. 21518 * This method is useful for operations that depend on the path's current endpoint, 21519 * such as connecting subsequent shapes or paths. It supports various drawing instructions, 21520 * ensuring the last point's position is accurately determined regardless of the path's complexity. 21521 * 21522 * If the last instruction is a `closePath`, the method iterates backward through the instructions 21523 * until it finds an actionable instruction that defines a point (e.g., `moveTo`, `lineTo`, 21524 * `quadraticCurveTo`, etc.). For compound paths added via `addPath`, it recursively retrieves 21525 * the last point from the nested path. 21526 * @param out - A `Point` object where the last point's coordinates will be stored. 21527 * This object is modified directly to contain the result. 21528 * @returns The `Point` object containing the last point's coordinates. 21529 */ 21530 getLastPoint(out) { 21531 let index = this.instructions.length - 1; 21532 let lastInstruction = this.instructions[index]; 21533 if (!lastInstruction) { 21534 out.x = 0; 21535 out.y = 0; 21536 return out; 21537 } 21538 while (lastInstruction.action === "closePath") { 21539 index--; 21540 if (index < 0) { 21541 out.x = 0; 21542 out.y = 0; 21543 return out; 21544 } 21545 lastInstruction = this.instructions[index]; 21546 } 21547 switch (lastInstruction.action) { 21548 case "moveTo": 21549 case "lineTo": 21550 out.x = lastInstruction.data[0]; 21551 out.y = lastInstruction.data[1]; 21552 break; 21553 case "quadraticCurveTo": 21554 out.x = lastInstruction.data[2]; 21555 out.y = lastInstruction.data[3]; 21556 break; 21557 case "bezierCurveTo": 21558 out.x = lastInstruction.data[4]; 21559 out.y = lastInstruction.data[5]; 21560 break; 21561 case "arc": 21562 case "arcToSvg": 21563 out.x = lastInstruction.data[5]; 21564 out.y = lastInstruction.data[6]; 21565 break; 21566 case "addPath": 21567 lastInstruction.data[0].getLastPoint(out); 21568 break; 21569 } 21570 return out; 21571 } 21572} 21573function adjustTransform(currentMatrix, transform) { 21574 if (currentMatrix) { 21575 return currentMatrix.prepend(transform); 21576 } 21577 return transform.clone(); 21578} 21579 21580function parseSVGFloatAttribute(svg, id, defaultValue) { 21581 const value = svg.getAttribute(id); 21582 return value ? Number(value) : defaultValue; 21583} 21584 21585function parseSVGDefinitions(svg, session) { 21586 const definitions = svg.querySelectorAll("defs"); 21587 for (let i = 0; i < definitions.length; i++) { 21588 const definition = definitions[i]; 21589 for (let j = 0; j < definition.children.length; j++) { 21590 const child = definition.children[j]; 21591 switch (child.nodeName.toLowerCase()) { 21592 case "lineargradient": 21593 session.defs[child.id] = parseLinearGradient(child); 21594 break; 21595 case "radialgradient": 21596 session.defs[child.id] = parseRadialGradient(); 21597 break; 21598 } 21599 } 21600 } 21601} 21602function parseLinearGradient(child) { 21603 const x0 = parseSVGFloatAttribute(child, "x1", 0); 21604 const y0 = parseSVGFloatAttribute(child, "y1", 0); 21605 const x1 = parseSVGFloatAttribute(child, "x2", 1); 21606 const y1 = parseSVGFloatAttribute(child, "y2", 0); 21607 const gradientUnit = child.getAttribute("gradientUnits") || "objectBoundingBox"; 21608 const gradient = new FillGradient( 21609 x0, 21610 y0, 21611 x1, 21612 y1, 21613 gradientUnit === "objectBoundingBox" ? "local" : "global" 21614 ); 21615 for (let k = 0; k < child.children.length; k++) { 21616 const stop = child.children[k]; 21617 const offset = parseSVGFloatAttribute(stop, "offset", 0); 21618 const color = Color.shared.setValue(stop.getAttribute("stop-color")).toNumber(); 21619 gradient.addColorStop(offset, color); 21620 } 21621 return gradient; 21622} 21623function parseRadialGradient(_child) { 21624 warn("[SVG Parser] Radial gradients are not yet supported"); 21625 return new FillGradient(0, 0, 1, 0); 21626} 21627 21628function extractSvgUrlId(url) { 21629 const match = url.match(/url\s*\(\s*['"]?\s*#([^'"\s)]+)\s*['"]?\s*\)/i); 21630 return match ? match[1] : ""; 21631} 21632 21633const styleAttributes = { 21634 // Fill properties 21635 fill: { type: "paint", default: 0 }, 21636 // Fill color/gradient 21637 "fill-opacity": { type: "number", default: 1 }, 21638 // Fill transparency 21639 // Stroke properties 21640 stroke: { type: "paint", default: 0 }, 21641 // Stroke color/gradient 21642 "stroke-width": { type: "number", default: 1 }, 21643 // Width of stroke 21644 "stroke-opacity": { type: "number", default: 1 }, 21645 // Stroke transparency 21646 "stroke-linecap": { type: "string", default: "butt" }, 21647 // End cap style: butt, round, square 21648 "stroke-linejoin": { type: "string", default: "miter" }, 21649 // Join style: miter, round, bevel 21650 "stroke-miterlimit": { type: "number", default: 10 },
vendor: 9,555 bytes, lines 21651-21951
21651 // Limit on miter join sharpness 21652 "stroke-dasharray": { type: "string", default: "none" }, 21653 // Dash pattern 21654 "stroke-dashoffset": { type: "number", default: 0 }, 21655 // Offset for dash pattern 21656 // Global properties 21657 opacity: { type: "number", default: 1 } 21658 // Overall opacity 21659}; 21660function parseSVGStyle(svg, session) { 21661 const style = svg.getAttribute("style"); 21662 const strokeStyle = {}; 21663 const fillStyle = {}; 21664 const result = { 21665 strokeStyle, 21666 fillStyle, 21667 useFill: false, 21668 useStroke: false 21669 }; 21670 for (const key in styleAttributes) { 21671 const attribute = svg.getAttribute(key); 21672 if (attribute) { 21673 parseAttribute(session, result, key, attribute.trim()); 21674 } 21675 } 21676 if (style) { 21677 const styleParts = style.split(";"); 21678 for (let i = 0; i < styleParts.length; i++) { 21679 const stylePart = styleParts[i].trim(); 21680 const [key, value] = stylePart.split(":"); 21681 if (styleAttributes[key]) { 21682 parseAttribute(session, result, key, value.trim()); 21683 } 21684 } 21685 } 21686 return { 21687 strokeStyle: result.useStroke ? strokeStyle : null, 21688 fillStyle: result.useFill ? fillStyle : null, 21689 useFill: result.useFill, 21690 useStroke: result.useStroke 21691 }; 21692} 21693function parseAttribute(session, result, id, value) { 21694 switch (id) { 21695 case "stroke": 21696 if (value !== "none") { 21697 if (value.startsWith("url(")) { 21698 const id2 = extractSvgUrlId(value); 21699 result.strokeStyle.fill = session.defs[id2]; 21700 } else { 21701 result.strokeStyle.color = Color.shared.setValue(value).toNumber(); 21702 } 21703 result.useStroke = true; 21704 } 21705 break; 21706 case "stroke-width": 21707 result.strokeStyle.width = Number(value); 21708 break; 21709 case "fill": 21710 if (value !== "none") { 21711 if (value.startsWith("url(")) { 21712 const id2 = extractSvgUrlId(value); 21713 result.fillStyle.fill = session.defs[id2]; 21714 } else { 21715 result.fillStyle.color = Color.shared.setValue(value).toNumber(); 21716 } 21717 result.useFill = true; 21718 } 21719 break; 21720 case "fill-opacity": 21721 result.fillStyle.alpha = Number(value); 21722 break; 21723 case "stroke-opacity": 21724 result.strokeStyle.alpha = Number(value); 21725 break; 21726 case "opacity": 21727 result.fillStyle.alpha = Number(value); 21728 result.strokeStyle.alpha = Number(value); 21729 break; 21730 } 21731} 21732 21733function checkForNestedPattern(subpathsWithArea) { 21734 if (subpathsWithArea.length <= 2) { 21735 return true; 21736 } 21737 const areas = subpathsWithArea.map((s) => s.area).sort((a, b) => b - a); 21738 const [largestArea, secondArea] = areas; 21739 const smallestArea = areas[areas.length - 1]; 21740 const largestToSecondRatio = largestArea / secondArea; 21741 const secondToSmallestRatio = secondArea / smallestArea; 21742 if (largestToSecondRatio > 3 && secondToSmallestRatio < 2) { 21743 return false; 21744 } 21745 return true; 21746} 21747 21748function extractSubpaths(pathData) { 21749 const parts = pathData.split(/(?=[Mm])/); 21750 const subpaths = parts.filter((part) => part.trim().length > 0); 21751 return subpaths; 21752} 21753function calculatePathArea(pathData) { 21754 const coords = pathData.match(/[-+]?[0-9]*\.?[0-9]+/g); 21755 if (!coords || coords.length < 4) 21756 return 0; 21757 const numbers = coords.map(Number); 21758 const xs = []; 21759 const ys = []; 21760 for (let i = 0; i < numbers.length; i += 2) { 21761 if (i + 1 < numbers.length) { 21762 xs.push(numbers[i]); 21763 ys.push(numbers[i + 1]); 21764 } 21765 } 21766 if (xs.length === 0 || ys.length === 0) 21767 return 0; 21768 const minX = Math.min(...xs); 21769 const maxX = Math.max(...xs); 21770 const minY = Math.min(...ys); 21771 const maxY = Math.max(...ys); 21772 const area = (maxX - minX) * (maxY - minY); 21773 return area; 21774} 21775function appendSVGPath(pathData, graphicsPath) { 21776 const tempPath = new GraphicsPath(pathData, false); 21777 for (const instruction of tempPath.instructions) { 21778 graphicsPath.instructions.push(instruction); 21779 } 21780} 21781 21782function SVGParser(svg, graphicsContext) { 21783 if (typeof svg === "string") { 21784 const div = document.createElement("div"); 21785 div.innerHTML = svg.trim(); 21786 svg = div.querySelector("svg"); 21787 } 21788 const session = { 21789 context: graphicsContext, 21790 defs: {}, 21791 path: new GraphicsPath() 21792 }; 21793 parseSVGDefinitions(svg, session); 21794 const children = svg.children; 21795 const { fillStyle, strokeStyle } = parseSVGStyle(svg, session); 21796 for (let i = 0; i < children.length; i++) { 21797 const child = children[i]; 21798 if (child.nodeName.toLowerCase() === "defs") 21799 continue; 21800 renderChildren(child, session, fillStyle, strokeStyle); 21801 } 21802 return graphicsContext; 21803} 21804function renderChildren(svg, session, fillStyle, strokeStyle) { 21805 const children = svg.children; 21806 const { fillStyle: f1, strokeStyle: s1 } = parseSVGStyle(svg, session); 21807 if (f1 && fillStyle) { 21808 fillStyle = { ...fillStyle, ...f1 }; 21809 } else if (f1) { 21810 fillStyle = f1; 21811 } 21812 if (s1 && strokeStyle) { 21813 strokeStyle = { ...strokeStyle, ...s1 }; 21814 } else if (s1) { 21815 strokeStyle = s1; 21816 } 21817 const noStyle = !fillStyle && !strokeStyle; 21818 if (noStyle) { 21819 fillStyle = { color: 0 }; 21820 } 21821 let x; 21822 let y; 21823 let x1; 21824 let y1; 21825 let x2; 21826 let y2; 21827 let cx; 21828 let cy; 21829 let r; 21830 let rx; 21831 let ry; 21832 let points; 21833 let pointsString; 21834 let d; 21835 let graphicsPath; 21836 let width; 21837 let height; 21838 switch (svg.nodeName.toLowerCase()) { 21839 case "path": { 21840 d = svg.getAttribute("d"); 21841 const fillRule = svg.getAttribute("fill-rule"); 21842 const subpaths = extractSubpaths(d); 21843 const hasExplicitEvenodd = fillRule === "evenodd"; 21844 const hasMultipleSubpaths = subpaths.length > 1; 21845 const shouldProcessHoles = hasExplicitEvenodd && hasMultipleSubpaths; 21846 if (shouldProcessHoles) { 21847 const subpathsWithArea = subpaths.map((subpath) => ({ 21848 path: subpath, 21849 area: calculatePathArea(subpath) 21850 })); 21851 subpathsWithArea.sort((a, b) => b.area - a.area); 21852 const useMultipleHolesApproach = subpaths.length > 3 || !checkForNestedPattern(subpathsWithArea); 21853 if (useMultipleHolesApproach) { 21854 for (let i = 0; i < subpathsWithArea.length; i++) { 21855 const subpath = subpathsWithArea[i]; 21856 const isMainShape = i === 0; 21857 session.context.beginPath(); 21858 const newPath = new GraphicsPath(void 0, true); 21859 appendSVGPath(subpath.path, newPath); 21860 session.context.path(newPath); 21861 if (isMainShape) { 21862 if (fillStyle) 21863 session.context.fill(fillStyle); 21864 if (strokeStyle) 21865 session.context.stroke(strokeStyle); 21866 } else { 21867 session.context.cut(); 21868 } 21869 } 21870 } else { 21871 for (let i = 0; i < subpathsWithArea.length; i++) { 21872 const subpath = subpathsWithArea[i]; 21873 const isHole = i % 2 === 1; 21874 session.context.beginPath(); 21875 const newPath = new GraphicsPath(void 0, true); 21876 appendSVGPath(subpath.path, newPath); 21877 session.context.path(newPath); 21878 if (isHole) { 21879 session.context.cut(); 21880 } else { 21881 if (fillStyle) 21882 session.context.fill(fillStyle); 21883 if (strokeStyle) 21884 session.context.stroke(strokeStyle); 21885 } 21886 } 21887 } 21888 } else { 21889 const useEvenoddForGraphicsPath = fillRule ? fillRule === "evenodd" : true; 21890 graphicsPath = new GraphicsPath(d, useEvenoddForGraphicsPath); 21891 session.context.path(graphicsPath); 21892 if (fillStyle) 21893 session.context.fill(fillStyle); 21894 if (strokeStyle) 21895 session.context.stroke(strokeStyle); 21896 } 21897 break; 21898 } 21899 case "circle": 21900 cx = parseSVGFloatAttribute(svg, "cx", 0); 21901 cy = parseSVGFloatAttribute(svg, "cy", 0); 21902 r = parseSVGFloatAttribute(svg, "r", 0); 21903 session.context.ellipse(cx, cy, r, r); 21904 if (fillStyle) 21905 session.context.fill(fillStyle); 21906 if (strokeStyle) 21907 session.context.stroke(strokeStyle); 21908 break; 21909 case "rect": 21910 x = parseSVGFloatAttribute(svg, "x", 0); 21911 y = parseSVGFloatAttribute(svg, "y", 0); 21912 width = parseSVGFloatAttribute(svg, "width", 0); 21913 height = parseSVGFloatAttribute(svg, "height", 0); 21914 rx = parseSVGFloatAttribute(svg, "rx", 0); 21915 ry = parseSVGFloatAttribute(svg, "ry", 0); 21916 if (rx || ry) { 21917 session.context.roundRect(x, y, width, height, rx || ry); 21918 } else { 21919 session.context.rect(x, y, width, height); 21920 } 21921 if (fillStyle) 21922 session.context.fill(fillStyle); 21923 if (strokeStyle) 21924 session.context.stroke(strokeStyle); 21925 break; 21926 case "ellipse": 21927 cx = parseSVGFloatAttribute(svg, "cx", 0); 21928 cy = parseSVGFloatAttribute(svg, "cy", 0); 21929 rx = parseSVGFloatAttribute(svg, "rx", 0); 21930 ry = parseSVGFloatAttribute(svg, "ry", 0); 21931 session.context.beginPath(); 21932 session.context.ellipse(cx, cy, rx, ry); 21933 if (fillStyle) 21934 session.context.fill(fillStyle); 21935 if (strokeStyle) 21936 session.context.stroke(strokeStyle); 21937 break; 21938 case "line": 21939 x1 = parseSVGFloatAttribute(svg, "x1", 0); 21940 y1 = parseSVGFloatAttribute(svg, "y1", 0); 21941 x2 = parseSVGFloatAttribute(svg, "x2", 0); 21942 y2 = parseSVGFloatAttribute(svg, "y2", 0); 21943 session.context.beginPath(); 21944 session.context.moveTo(x1, y1); 21945 session.context.lineTo(x2, y2); 21946 if (strokeStyle) 21947 session.context.stroke(strokeStyle); 21948 break; 21949 case "polygon": 21950 pointsString = svg.getAttribute("points"); 21951 points = pointsString.match(/-?\d+/g).map((n) => parseInt(n, 10));
21952 session.context.poly(points, true); 21953 if (fillStyle) 21954 session.context.fill(fillStyle); 21955 if (strokeStyle) 21956 session.context.stroke(strokeStyle); 21957 break; 21958 case "polyline": 21959 pointsString = svg.getAttribute("points"); 21960 points = pointsString.match(/-?\d+/g).map((n) => parseInt(n, 10)); 21961 session.context.poly(points, false); 21962 if (strokeStyle) 21963 session.context.stroke(strokeStyle); 21964 break; 21965 case "g": 21966 case "svg": 21967 break; 21968 default: { 21969 warn(`[SVG parser] <${svg.nodeName}> elements unsupported`); 21970 break; 21971 } 21972 } 21973 if (noStyle) { 21974 fillStyle = null; 21975 } 21976 for (let i = 0; i < children.length; i++) { 21977 renderChildren(children[i], session, fillStyle, strokeStyle); 21978 } 21979} 21980 21981function isColorLike(value) { 21982 return Color.isColorLike(value); 21983} 21984function isFillPattern(value) { 21985 return value instanceof FillPattern; 21986} 21987function isFillGradient(value) { 21988 return value instanceof FillGradient; 21989} 21990function isTexture(value) { 21991 return value instanceof Texture; 21992} 21993function handleColorLike(fill, value, defaultStyle) { 21994 const temp = Color.shared.setValue(value ?? 0); 21995 fill.color = temp.toNumber(); 21996 fill.alpha = temp.alpha === 1 ? defaultStyle.alpha : temp.alpha; 21997 fill.texture = Texture.WHITE; 21998 return { ...defaultStyle, ...fill }; 21999} 22000function handleTexture(fill, value, defaultStyle) { 22001 fill.texture = value; 22002 return { ...defaultStyle, ...fill }; 22003} 22004function handleFillPattern(fill, value, defaultStyle) { 22005 fill.fill = value; 22006 fill.color = 16777215; 22007 fill.texture = value.texture; 22008 fill.matrix = value.transform; 22009 return { ...defaultStyle, ...fill }; 22010} 22011function handleFillGradient(fill, value, defaultStyle) { 22012 value.buildGradient(); 22013 fill.fill = value; 22014 fill.color = 16777215; 22015 fill.texture = value.texture; 22016 fill.matrix = value.transform; 22017 fill.textureSpace = value.textureSpace; 22018 return { ...defaultStyle, ...fill }; 22019} 22020function handleFillObject(value, defaultStyle) { 22021 const style = { ...defaultStyle, ...value }; 22022 const color = Color.shared.setValue(style.color); 22023 style.alpha *= color.alpha; 22024 style.color = color.toNumber(); 22025 return style; 22026} 22027function toFillStyle(value, defaultStyle) { 22028 if (value === void 0 || value === null) { 22029 return null; 22030 } 22031 const fill = {}; 22032 const objectStyle = value; 22033 if (isColorLike(value)) { 22034 return handleColorLike(fill, value, defaultStyle); 22035 } else if (isTexture(value)) { 22036 return handleTexture(fill, value, defaultStyle); 22037 } else if (isFillPattern(value)) { 22038 return handleFillPattern(fill, value, defaultStyle); 22039 } else if (isFillGradient(value)) { 22040 return handleFillGradient(fill, value, defaultStyle); 22041 } else if (objectStyle.fill && isFillPattern(objectStyle.fill)) { 22042 return handleFillPattern(objectStyle, objectStyle.fill, defaultStyle); 22043 } else if (objectStyle.fill && isFillGradient(objectStyle.fill)) { 22044 return handleFillGradient(objectStyle, objectStyle.fill, defaultStyle); 22045 } 22046 return handleFillObject(objectStyle, defaultStyle); 22047} 22048function toStrokeStyle(value, defaultStyle) { 22049 const { width, alignment, miterLimit, cap, join, pixelLine, ...rest } = defaultStyle; 22050 const fill = toFillStyle(value, rest); 22051 if (!fill) { 22052 return null; 22053 } 22054 return { 22055 width,
22056 alignment, 22057 miterLimit, 22058 cap, 22059 join, 22060 pixelLine, 22061 ...fill 22062 }; 22063} 22064 22065const tmpPoint = new Point(); 22066const tempMatrix = new Matrix(); 22067const _GraphicsContext = class _GraphicsContext extends EventEmitter { 22068 constructor() { 22069 super(...arguments); 22070 /** @internal */ 22071 this._gpuData = /* @__PURE__ */ Object.create(null); 22072 /** If set to true, the resource will be garbage collected automatically when it is not used. */ 22073 this.autoGarbageCollect = true; 22074 /** @internal */ 22075 this._gcLastUsed = -1; 22076 /** 22077 * unique id for this graphics context 22078 * @internal 22079 */ 22080 this.uid = uid("graphicsContext"); 22081 /** 22082 * Indicates whether content is updated and have to be re-rendered. 22083 * @internal 22084 */ 22085 this.dirty = true; 22086 /** The batch mode for this graphics context. It can be 'auto', 'batch', or 'no-batch'. */ 22087 this.batchMode = "auto"; 22088 /** @internal */ 22089 this.instructions = []; 22090 /** Whether the graphics context has been destroyed. */ 22091 this.destroyed = false; 22092 this._activePath = new GraphicsPath(); 22093 this._transform = new Matrix(); 22094 this._fillStyle = { ..._GraphicsContext.defaultFillStyle }; 22095 this._strokeStyle = { ..._GraphicsContext.defaultStrokeStyle }; 22096 this._stateStack = []; 22097 this._tick = 0; 22098 this._bounds = new Bounds(); 22099 this._boundsDirty = true; 22100 } 22101 /** 22102 * Creates a new GraphicsContext object that is a clone of this instance, copying all properties, 22103 * including the current drawing state, transformations, styles, and instructions. 22104 * @returns A new GraphicsContext instance with the same properties and state as this one. 22105 */ 22106 clone() { 22107 const clone = new _GraphicsContext(); 22108 clone.batchMode = this.batchMode; 22109 clone.instructions = this.instructions.slice(); 22110 clone._activePath = this._activePath.clone(); 22111 clone._transform = this._transform.clone(); 22112 clone._fillStyle = { ...this._fillStyle }; 22113 clone._strokeStyle = { ...this._strokeStyle }; 22114 clone._stateStack = this._stateStack.slice(); 22115 clone._bounds = this._bounds.clone(); 22116 clone._boundsDirty = true; 22117 return clone; 22118 } 22119 /** 22120 * The current fill style of the graphics context. This can be a color, gradient, pattern, or a more complex style defined by a FillStyle object. 22121 */ 22122 get fillStyle() { 22123 return this._fillStyle; 22124 } 22125 set fillStyle(value) { 22126 this._fillStyle = toFillStyle(value, _GraphicsContext.defaultFillStyle); 22127 } 22128 /** 22129 * The current stroke style of the graphics context. Similar to fill styles, stroke styles can encompass colors, gradients, patterns, or more detailed configurations via a StrokeStyle object. 22130 */ 22131 get strokeStyle() { 22132 return this._strokeStyle; 22133 } 22134 set strokeStyle(value) { 22135 this._strokeStyle = toStrokeStyle(value, _GraphicsContext.defaultStrokeStyle); 22136 } 22137 /** 22138 * Sets the current fill style of the graphics context. The fill style can be a color, gradient, 22139 * pattern, or a more complex style defined by a FillStyle object. 22140 * @param style - The fill style to apply. This can be a simple color, a gradient or pattern object, 22141 * or a FillStyle or ConvertedFillStyle object. 22142 * @returns The instance of the current GraphicsContext for method chaining. 22143 */ 22144 setFillStyle(style) { 22145 this._fillStyle = toFillStyle(style, _GraphicsContext.defaultFillStyle); 22146 return this; 22147 } 22148 /** 22149 * Sets the current stroke style of the graphics context. Similar to fill styles, stroke styles can 22150 * encompass colors, gradients, patterns, or more detailed configurations via a StrokeStyle object. 22151 * @param style - The stroke style to apply. Can be defined as a color, a gradient or pattern, 22152 * or a StrokeStyle or ConvertedStrokeStyle object. 22153 * @returns The instance of the current GraphicsContext for method chaining. 22154 */ 22155 setStrokeStyle(style) { 22156 this._strokeStyle = toFillStyle(style, _GraphicsContext.defaultStrokeStyle); 22157 return this; 22158 } 22159 texture(texture, tint, dx, dy, dw, dh) { 22160 this.instructions.push({ 22161 action: "texture", 22162 data: { 22163 image: texture, 22164 dx: dx || 0, 22165 dy: dy || 0, 22166 dw: dw || texture.frame.width, 22167 dh: dh || texture.frame.height, 22168 transform: this._transform.clone(), 22169 alpha: this._fillStyle.alpha, 22170 style: tint ? Color.shared.setValue(tint).toNumber() : 16777215 22171 } 22172 }); 22173 this.onUpdate(); 22174 return this; 22175 } 22176 /** 22177 * Resets the current path. Any previous path and its commands are discarded and a new path is 22178 * started. This is typically called before beginning a new shape or series of drawing commands. 22179 * @returns The instance of the current GraphicsContext for method chaining. 22180 */ 22181 beginPath() { 22182 this._activePath = new GraphicsPath(); 22183 return this; 22184 } 22185 fill(style, alpha) { 22186 let path; 22187 const lastInstruction = this.instructions[this.instructions.length - 1]; 22188 if (this._tick === 0 && lastInstruction?.action === "stroke") { 22189 path = lastInstruction.data.path; 22190 } else { 22191 path = this._activePath.clone(); 22192 } 22193 if (!path) 22194 return this; 22195 if (style != null) { 22196 if (alpha !== void 0 && typeof style === "number") { 22197 deprecation(v8_0_0, "GraphicsContext.fill(color, alpha) is deprecated, use GraphicsContext.fill({ color, alpha }) instead"); 22198 style = { color: style, alpha }; 22199 } 22200 this._fillStyle = toFillStyle(style, _GraphicsContext.defaultFillStyle); 22201 } 22202 this.instructions.push({ 22203 action: "fill", 22204 // TODO copy fill style! 22205 data: { style: this.fillStyle, path } 22206 }); 22207 this.onUpdate(); 22208 this._initNextPathLocation(); 22209 this._tick = 0; 22210 return this; 22211 } 22212 _initNextPathLocation() { 22213 const { x, y } = this._activePath.getLastPoint(Point.shared); 22214 this._activePath.clear(); 22215 this._activePath.moveTo(x, y); 22216 } 22217 /** 22218 * Strokes the current path with the current stroke style. This method can take an optional 22219 * FillInput parameter to define the stroke's appearance, including its color, width, and other properties. 22220 * @param style - (Optional) The stroke style to apply. Can be defined as a simple c
22220olor or a more complex style object. If omitted, uses the current stroke style. 22221 * @returns The instance of the current GraphicsContext for method chaining. 22222 */ 22223 stroke(style) { 22224 let path; 22225 const lastInstruction = this.instructions[this.instructions.length - 1]; 22226 if (this._tick === 0 && lastInstruction?.action === "fill") { 22227 path = lastInstruction.data.path; 22228 } else { 22229 path = this._activePath.clone(); 22230 } 22231 if (!path) 22232 return this; 22233 if (style != null) { 22234 this._strokeStyle = toStrokeStyle(style, _GraphicsContext.defaultStrokeStyle); 22235 } 22236 this.instructions.push({ 22237 action: "stroke", 22238 // TODO copy fill style! 22239 data: { style: this.strokeStyle, path } 22240 }); 22241 this.onUpdate(); 22242 this._initNextPathLocation(); 22243 this._tick = 0; 22244 return this; 22245 } 22246 /** 22247 * Applies a cutout to the last drawn shape. This is used to create holes or complex shapes by 22248 * subtracting a path from the previously drawn path. If a hole is not completely in a shape, it will 22249 * fail to cut correctly! 22250 * @returns The instance of the current GraphicsContext for method chaining. 22251 */ 22252 cut() { 22253 for (let i = 0; i < 2; i++) { 22254 const lastInstruction = this.instructions[this.instructions.length - 1 - i]; 22255 const holePath = this._activePath.clone(); 22256 if (lastInstruction) { 22257 if (lastInstruction.action === "stroke" || lastInstruction.action === "fill") { 22258 if (lastInstruction.data.hole) { 22259 lastInstruction.data.hole.addPath(holePath); 22260 } else { 22261 lastInstruction.data.hole = holePath; 22262 break; 22263 } 22264 } 22265 } 22266 } 22267 this._initNextPathLocation(); 22268 return this; 22269 } 22270 /** 22271 * Adds an arc to the current path, which is centered at (x, y) with the specified radius, 22272 * starting and ending angles, and direction. 22273 * @param x - The x-coordinate of the arc's center. 22274 * @param y - The y-coordinate of the arc's center. 22275 * @param radius - The arc's radius. 22276 * @param startAngle - The starting angle, in radians. 22277 * @param endAngle - The ending angle, in radians. 22278 * @param counterclockwise - (Optional) Specifies whether the arc is drawn counterclockwise (true) or clockwise (false). Defaults to false. 22279 * @returns The instance of the current GraphicsContext for method chaining. 22280 */ 22281 arc(x, y, radius, startAngle, endAngle, counterclockwise) { 22282 this._tick++; 22283 const t = this._transform; 22284 this._activePath.arc( 22285 t.a * x + t.c * y + t.tx, 22286 t.b * x + t.d * y + t.ty, 22287 radius, 22288 startAngle, 22289 endAngle, 22290 counterclockwise 22291 ); 22292 return this; 22293 } 22294 /** 22295 * Adds an arc to the current path with the given control points and radius, connected to the previous point 22296 * by a straight line if necessary. 22297 * @param x1 - The x-coordinate of the first control point. 22298 * @param y1 - The y-coordinate of the first control point. 22299 * @param x2 - The x-coordinate of the second control point. 22300 * @param y2 - The y-coordinate of the second control point. 22301 * @param radius - The arc's radius. 22302 * @returns The instance of the current GraphicsContext for method chaining. 22303 */ 22304 arcTo(x1, y1, x2, y2, radius) { 22305 this._tick++; 22306 const t = this._transform; 22307 this._activePath.arcTo( 22308 t.a * x1 + t.c * y1 + t.tx, 22309 t.b * x1 + t.d * y1 + t.ty, 22310 t.a * x2 + t.c * y2 + t.tx, 22311 t.b * x2 + t.d * y2 + t.ty, 22312 radius 22313 ); 22314 return this; 22315 } 22316 /** 22317 * Adds an SVG-style arc to the path, allowing for elliptical arcs based on the SVG spec. 22318 * @param rx - The x-radius of the ellipse. 22319 * @param ry - The y-radius of the ellipse. 22320 * @param xAxisRotation - The rotation of the ellipse's x-axis relative 22321 * to the x-axis of the coordinate system, in degrees. 22322 * @param largeArcFlag - Determines if the arc should be greater than or less than 180 degrees. 22323 * @param sweepFlag - Determines if the arc should be swept in a positive angle direction. 22324 * @param x - The x-coordinate of the arc's end point. 22325 * @param y - The y-coordinate of the arc's end point. 22326 * @returns The instance of the current object for chaining. 22327 */ 22328 arcToSvg(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y) { 22329 this._tick++; 22330 const t = this._transform; 22331 this._activePath.arcToSvg( 22332 rx, 22333 ry, 22334 xAxisRotation, 22335 // should we rotate this with transform?? 22336 largeArcFlag, 22337 sweepFlag, 22338 t.a * x + t.c * y + t.tx, 22339 t.b * x + t.d * y + t.ty 22340 ); 22341 return this; 22342 } 22343 /** 22344 * Adds a cubic Bezier curve to the path.
22345 * It requires three points: the first two are control points and the third one is the end point. 22346 * The starting point is the last point in the current path. 22347 * @param cp1x - The x-coordinate of the first control point. 22348 * @param cp1y - The y-coordinate of the first control point. 22349 * @param cp2x - The x-coordinate of the second control point. 22350 * @param cp2y - The y-coordinate of the second control point. 22351 * @param x - The x-coordinate of the end point. 22352 * @param y - The y-coordinate of the end point. 22353 * @param smoothness - Optional parameter to adjust the smoothness of the curve. 22354 * @returns The instance of the current object for chaining. 22355 */ 22356 bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y, smoothness) { 22357 this._tick++; 22358 const t = this._transform; 22359 this._activePath.bezierCurveTo( 22360 t.a * cp1x + t.c * cp1y + t.tx, 22361 t.b * cp1x + t.d * cp1y + t.ty, 22362 t.a * cp2x + t.c * cp2y + t.tx, 22363 t.b * cp2x + t.d * cp2y + t.ty, 22364 t.a * x + t.c * y + t.tx, 22365 t.b * x + t.d * y + t.ty, 22366 smoothness 22367 ); 22368 return this; 22369 } 22370 /** 22371 * Closes the current path by drawing a straight line back to the start. 22372 * If the shape is already closed or there are no points in the path, this method does nothing. 22373 * @returns The instance of the current object for chaining. 22374 */ 22375 closePath() { 22376 this._tick++; 22377 this._activePath?
vendor: 11,602 bytes, lines 22377-22643
22377.closePath(); 22378 return this; 22379 } 22380 /** 22381 * Draws an ellipse at the specified location and with the given x and y radii. 22382 * An optional transformation can be applied, allowing for rotation, scaling, and translation. 22383 * @param x - The x-coordinate of the center of the ellipse. 22384 * @param y - The y-coordinate of the center of the ellipse. 22385 * @param radiusX - The horizontal radius of the ellipse. 22386 * @param radiusY - The vertical radius of the ellipse. 22387 * @returns The instance of the current object for chaining. 22388 */ 22389 ellipse(x, y, radiusX, radiusY) { 22390 this._tick++; 22391 this._activePath.ellipse(x, y, radiusX, radiusY, this._transform.clone()); 22392 return this; 22393 } 22394 /** 22395 * Draws a circle shape. This method adds a new circle path to the current drawing. 22396 * @param x - The x-coordinate of the center of the circle. 22397 * @param y - The y-coordinate of the center of the circle. 22398 * @param radius - The radius of the circle. 22399 * @returns The instance of the current object for chaining. 22400 */ 22401 circle(x, y, radius) { 22402 this._tick++; 22403 this._activePath.circle(x, y, radius, this._transform.clone()); 22404 return this; 22405 } 22406 /** 22407 * Adds another `GraphicsPath` to this path, optionally applying a transformation. 22408 * @param path - The `GraphicsPath` to add. 22409 * @returns The instance of the current object for chaining. 22410 */ 22411 path(path) { 22412 this._tick++; 22413 this._activePath.addPath(path, this._transform.clone()); 22414 return this; 22415 } 22416 /** 22417 * Connects the current point to a new point with a straight line. This method updates the current path. 22418 * @param x - The x-coordinate of the new point to connect to. 22419 * @param y - The y-coordinate of the new point to connect to. 22420 * @returns The instance of the current object for chaining. 22421 */ 22422 lineTo(x, y) { 22423 this._tick++; 22424 const t = this._transform; 22425 this._activePath.lineTo( 22426 t.a * x + t.c * y + t.tx, 22427 t.b * x + t.d * y + t.ty 22428 ); 22429 return this; 22430 } 22431 /** 22432 * Sets the starting point for a new sub-path. Any subsequent drawing commands are considered part of this path. 22433 * @param x - The x-coordinate for the starting point. 22434 * @param y - The y-coordinate for the starting point. 22435 * @returns The instance of the current object for chaining. 22436 */ 22437 moveTo(x, y) { 22438 this._tick++; 22439 const t = this._transform; 22440 const instructions = this._activePath.instructions; 22441 const transformedX = t.a * x + t.c * y + t.tx; 22442 const transformedY = t.b * x + t.d * y + t.ty; 22443 if (instructions.length === 1 && instructions[0].action === "moveTo") { 22444 instructions[0].data[0] = transformedX; 22445 instructions[0].data[1] = transformedY; 22446 return this; 22447 } 22448 this._activePath.moveTo( 22449 transformedX, 22450 transformedY 22451 ); 22452 return this; 22453 } 22454 /** 22455 * Adds a quadratic curve to the path. It requires two points: the control point and the end point. 22456 * The starting point is the last point in the current path. 22457 * @param cpx - The x-coordinate of the control point. 22458 * @param cpy - The y-coordinate of the control point. 22459 * @param x - The x-coordinate of the end point. 22460 * @param y - The y-coordinate of the end point. 22461 * @param smoothness - Optional parameter to adjust the smoothness of the curve. 22462 * @returns The instance of the current object for chaining. 22463 */ 22464 quadraticCurveTo(cpx, cpy, x, y, smoothness) { 22465 this._tick++; 22466 const t = this._transform; 22467 this._activePath.quadraticCurveTo( 22468 t.a * cpx + t.c * cpy + t.tx, 22469 t.b * cpx + t.d * cpy + t.ty, 22470 t.a * x + t.c * y + t.tx, 22471 t.b * x + t.d * y + t.ty, 22472 smoothness 22473 ); 22474 return this; 22475 } 22476 /** 22477 * Draws a rectangle shape. This method adds a new rectangle path to the current drawing. 22478 * @param x - The x-coordinate of the top-left corner of the rectangle. 22479 * @param y - The y-coordinate of the top-left corner of the rectangle. 22480 * @param w - The width of the rectangle. 22481 * @param h - The height of the rectangle. 22482 * @returns The instance of the current object for chaining. 22483 */ 22484 rect(x, y, w, h) { 22485 this._tick++; 22486 this._activePath.rect(x, y, w, h, this._transform.clone()); 22487 return this; 22488 } 22489 /** 22490 * Draws a rectangle with rounded corners. 22491 * The corner radius can be specified to determine how rounded the corners should be. 22492 * An optional transformation can be applied, which allows for rotation, scaling, and translation of the rectangle. 22493 * @param x - The x-coordinate of the top-left corner of the rectangle. 22494 * @param y - The y-coordinate of the top-left corner of the rectangle. 22495 * @param w - The width of the rectangle. 22496 * @param h - The height of the rectangle. 22497 * @param radius - The radius of the rectangle's corners. If not specified, corners will be sharp. 22498 * @returns The instance of the current object for chaining. 22499 */ 22500 roundRect(x, y, w, h, radius) { 22501 this._tick++; 22502 this._activePath.roundRect(x, y, w, h, radius, this._transform.clone()); 22503 return this; 22504 } 22505 /** 22506 * Draws a polygon shape by specifying a sequence of points. This method allows for the creation of complex polygons, 22507 * which can be both open and closed. An optional transformation can be applied, enabling the polygon to be scaled, 22508 * rotated, or translated as needed. 22509 * @param points - An array of numbers, or an array of PointData objects eg [{x,y}, {x,y}, {x,y}] 22510 * representing the x and y coordinates, of the polygon's vertices, in sequence. 22511 * @param close - A boolean indicating whether to close the polygon path. True by default. 22512 */ 22513 poly(points, close) { 22514 this._tick++; 22515 this._activePath.poly(points, close, this._transform.clone()); 22516 return this; 22517 } 22518 /** 22519 * Draws a regular polygon with a specified number of sides. All sides and angles are equal. 22520 * @param x - The x-coordinate of the center of the polygon. 22521 * @param y - The y-coordinate of the center of the polygon. 22522 * @param radius - The radius of the circumscribed circle of the polygon. 22523 * @param sides - The number of sides of the polygon. Must be 3 or more. 22524 * @param rotation - The rotation angle of the polygon, in radians. Zero by default. 22525 * @param transform - An optional `Matrix` object to apply a transformation to the polygon. 22526 * @returns The instance of the current object for chaining. 22527 */ 22528 regularPoly(x, y, radius, sides, rotation = 0, transform) { 22529 this._tick++; 22530 this._activePath.regularPoly(x, y, radius, sides, rotation, transform); 22531 return this; 22532 } 22533 /** 22534 * Draws a polygon with rounded corners. 22535 * Similar to `regularPoly` but with the ability to round the corners of the polygon. 22536 * @param x - The x-coordinate of the center of the polygon. 22537 * @param y - The y-coordinate of the center of the polygon. 22538 * @param radius - The radius of the circumscribed circle of the polygon. 22539 * @param sides - The number of sides of the polygon. Must be 3 or more. 22540 * @param corner - The radius of the rounding of the corners. 22541 * @param rotation - The rotation angle of the polygon, in radians. Zero by default. 22542 * @returns The instance of the current object for chaining. 22543 */ 22544 roundPoly(x, y, radius, sides, corner, rotation) { 22545 this._tick++; 22546 this._activePath.roundPoly(x, y, radius, sides, corner, rotation); 22547 return this; 22548 } 22549 /** 22550 * Draws a shape with rounded corners. This function supports custom radius for each corner of the shape. 22551 * Optionally, corners can be rounded using a quadratic curve instead of an arc, providing a different aesthetic. 22552 * @param points - An array of `RoundedPoint` representing the corners of the shape to draw. 22553 * A minimum of 3 points is required. 22554 * @param radius - The default radius for the corners. 22555 * This radius is applied to all corners unless overridden in `points`. 22556 * @param useQuadratic - If set to true, rounded corners are drawn using a quadraticCurve 22557 * method instead of an arc method. Defaults to false. 22558 * @param smoothness - Specifies the smoothness of the curve when `useQuadratic` is true. 22559 * Higher values make the curve smoother. 22560 * @returns The instance of the current object for chaining. 22561 */ 22562 roundShape(points, radius, useQuadratic, smoothness) { 22563 this._tick++; 22564 this._activePath.roundShape(points, radius, useQuadratic, smoothness); 22565 return this; 22566 } 22567 /** 22568 * Draw Rectangle with fillet corners. This is much like rounded rectangle 22569 * however it support negative numbers as well for the corner radius. 22570 * @param x - Upper left corner of rect 22571 * @param y - Upper right corner of rect 22572 * @param width - Width of rect 22573 * @param height - Height of rect 22574 * @param fillet - accept negative or positive values 22575 */ 22576 filletRect(x, y, width, height, fillet) { 22577 this._tick++; 22578 this._activePath.filletRect(x, y, width, height, fillet); 22579 return this; 22580 } 22581 /** 22582 * Draw Rectangle with chamfer corners. These are angled corners. 22583 * @param x - Upper left corner of rect 22584 * @param y - Upper right corner of rect 22585 * @param width - Width of rect 22586 * @param height - Height of rect 22587 * @param chamfer - non-zero real number, size of corner cutout 22588 * @param transform 22589 */ 22590 chamferRect(x, y, width, height, chamfer, transform) { 22591 this._tick++; 22592 this._activePath.chamferRect(x, y, width, height, chamfer, transform); 22593 return this; 22594 } 22595 /** 22596 * Draws a star shape centered at a specified location. This method allows for the creation 22597 * of stars with a variable number of points, outer radius, optional inner radius, and rotation. 22598 * The star is drawn as a closed polygon with alternating outer and inner vertices to create the star's points. 22599 * An optional transformation can be applied to scale, rotate, or translate the star as needed. 22600 * @param x - The x-coordinate of the center of the star. 22601 * @param y - The y-coordinate of the center of the star. 22602 * @param points - The number of points of the star. 22603 * @param radius - The outer radius of the star (distance from the center to the outer points). 22604 * @param innerRadius - Optional. The inner radius of the star 22605 * (distance from the center to the inner points between the outer points). 22606 * If not provided, defaults to half of the `radius`. 22607 * @param rotation - Optional. The rotation of the star in radians, where 0 is aligned with the y-axis. 22608 * Defaults to 0, meaning one point is directly upward. 22609 * @returns The instance of the current object for chaining further drawing commands. 22610 */ 22611 star(x, y, points, radius, innerRadius = 0, rotation = 0) { 22612 this._tick++; 22613 this._activePath.star(x, y, points, radius, innerRadius, rotation, this._transform.clone()); 22614 return this; 22615 } 22616 /** 22617 * Parses and renders an SVG string into the graphics context. This allows for complex shapes and paths 22618 * defined in SVG format to be drawn within the graphics context. 22619 * @param svg - The SVG string to be parsed and rendered. 22620 */ 22621 svg(svg) { 22622 this._tick++; 22623 SVGParser(svg, this); 22624 return this; 22625 } 22626 /** 22627 * Restores the most recently saved graphics state by popping the top of the graphics state stack. 22628 * This includes transformations, fill styles, and stroke styles. 22629 */ 22630 restore() { 22631 const state = this._stateStack.pop(); 22632 if (state) { 22633 this._transform = state.transform; 22634 this._fillStyle = state.fillStyle; 22635 this._strokeStyle = state.strokeStyle; 22636 } 22637 return this; 22638 } 22639 /** Saves the current graphics state, including transformations, fill styles, and stroke styles, onto a stack. */ 22640 save() { 22641 this._stateStack.push({ 22642 transform: this._transform.clone(), 22643 fillStyle:
22643{ ...this._fillStyle }, 22644 strokeStyle: { ...this._strokeStyle } 22645 }); 22646 return this; 22647 } 22648 /** 22649 * Returns the current transformation matrix of the graphics context. 22650 * @returns The current transformation matrix. 22651 */ 22652 getTransform() { 22653 return this._transform; 22654 } 22655 /** 22656 * Resets the current transformation matrix to the identity matrix, effectively removing any transformations (rotation, scaling, translation) previously applied. 22657 * @returns The instance of the current GraphicsContext for method chaining. 22658 */ 22659 resetTransform() { 22660 this._transform.identity(); 22661 return this; 22662 } 22663 /** 22664 * Applies a rotation transformation to the graphics context around the current origin. 22665 * @param angle - The angle of rotation in radians. 22666 * @returns The instance of the current GraphicsContext for method chaining. 22667 */ 22668 rotate(angle) { 22669 this._transform.rotate(angle); 22670 return this; 22671 } 22672 /** 22673 * Applies a scaling transformation to the graphics context, scaling drawings by x horizontally and by y vertically. 22674 * @param x - The scale factor in the horizontal direction. 22675 * @param y - (Optional) The scale factor in the vertical direction. If not specified, the x value is used for both directions. 22676 * @returns The instance of the current GraphicsContext for method chaining. 22677 */ 22678 scale(x, y = x) { 22679 this._transform.scale(x, y); 22680 return this; 22681 } 22682 setTransform(a, b, c, d, dx, dy) { 22683 if (a instanceof Matrix) { 22684 this._transform.set(a.a, a.b, a.c, a.d, a.tx, a.ty); 22685 return this; 22686 } 22687 this._transform.set(a, b, c, d, dx, dy); 22688 return this; 22689 } 22690 transform(a, b, c, d, dx, dy) { 22691 if (a instanceof Matrix) { 22692 this._transform.append(a); 22693 return this; 22694 } 22695 tempMatrix.set(a, b, c, d, dx, dy); 22696 this._transform.append(tempMatrix); 22697 return this; 22698 } 22699 /** 22700 * Applies a translation transformation to the graphics context, moving the origin by the specified amounts. 22701 * @param x - The amount to translate in the horizontal direction. 22702 * @param y - (Optional) The amount to translate in the vertical direction. If not specified, the x value is used for both directions. 22703 * @returns The instance of the current GraphicsContext for method chaining. 22704 */ 22705 translate(x, y = x) { 22706 this._transform.translate(x, y); 22707 return this; 22708 } 22709 /** 22710 * Clears all drawing commands from the graphics context, effectively resetting it. This includes clearing the path, 22711 * and optionally resetting transformations to the identity matrix. 22712 * @returns The instance of the current GraphicsContext for method chaining. 22713 */ 22714 clear() { 22715 this._activePath.clear(); 22716 this.instructions.length = 0; 22717 this.resetTransform(); 22718 this.onUpdate(); 22719 return this; 22720 } 22721 onUpdate() { 22722 this._boundsDirty = true; 22723 if (this.dirty) 22724 return; 22725 this.emit("update", this, 16); 22726 this.dirty = true; 22727 } 22728 /** The bounds of the graphic shape. */ 22729 get bounds() { 22730 if (!this._boundsDirty) 22731 return this._bounds; 22732 this._boundsDirty = false; 22733 const bounds = this._bounds; 22734 bounds.clear(); 22735 for (let i = 0; i < this.instructions.length; i++) { 22736 const instruction = this.instructions[i]; 22737 const action = instruction.action; 22738 if (action === "fill") { 22739 const data = instruction.data; 22740 bounds.addBounds(data.path.bounds); 22741 } else if (action === "texture") { 22742 const data = instruction.data; 22743 bounds.addFrame(data.dx, data.dy, data.dx + data.dw, data.dy + data.dh, data.transform); 22744 } 22745 if (action === "stroke") { 22746 const data = instruction.data; 22747 const alignment = data.style.alignment; 22748 const outerPadding = data.style.width * (1 - alignment); 22749 const _bounds = data.path.bounds; 22750 bounds.addFrame( 22751 _bounds.minX - outerPadding, 22752 _bounds.minY - outerPadding, 22753 _bounds.maxX + outerPadding, 22754 _bounds.maxY + outerPadding 22755 ); 22756 } 22757 } 22758 return bounds; 22759 } 22760 /** 22761 * Check to see if a point is contained within this geometry. 22762 * @param point - Point to check if it's contained. 22763 * @returns {boolean} `true` if the point is contained within geometry. 22764 */ 22765 containsPoint(point) { 22766 if (!this.bounds.containsPoint(point.x, point.y)) 22767 return false;
22768 const instructions = this.instructions; 22769 let hasHit = false; 22770 for (let k = 0; k < instructions.length; k++) { 22771 const instruction = instructions[k]; 22772 const data = instruction.data; 22773 const path = data.path; 22774 if (!instruction.action || !path) 22775 continue; 22776 const style = data.style; 22777 const shapes = path.shapePath.shapePrimitives; 22778 for (let i = 0; i < shapes.length; i++) { 22779 const shape = shapes[i].shape; 22780 if (!style || !shape) 22781 continue; 22782 const transform = shapes[i].transform; 22783 const transformedPoint = transform ? transform.applyInverse(point, tmpPoint) : point; 22784 if (instruction.action === "fill") { 22785 hasHit = shape.contains(transformedPoint.x, transformedPoint.y); 22786 } else { 22787 const strokeStyle = style; 22788 hasHit = shape.strokeContains(transformedPoint.x, transformedPoint.y, strokeStyle.width, strokeStyle.alignment); 22789 } 22790 const holes = data.hole; 22791 if (holes) { 22792 const holeShapes = holes.shapePath?.shapePrimitives; 22793 if (holeShapes) { 22794 for (let j = 0; j < holeShapes.length; j++) { 22795 if (holeShapes[j].shape.contains(transformedPoint.x, transformedPoint.y)) { 22796 hasHit = false; 22797 } 22798 } 22799 } 22800 } 22801 if (hasHit) { 22802 return true; 22803 } 22804 } 22805 } 22806 return hasHit; 22807 } 22808 /** Unloads the GPU data from the graphics context. */ 22809 unload() { 22810 this.emit("unload", this); 22811 for (const key in this._gpuData) { 22812 this._gpuData[key]?.destroy(); 22813 } 22814 this._gpuData = /* @__PURE__ */ Object.create(null); 22815 } 22816 /** 22817 * Destroys the GraphicsData object. 22818 * @param options - Options parameter. A boolean will act as if all options 22819 * have been set to that value 22820 * @example 22821 * context.destroy(); 22822 * context.destroy(true); 22823 * context.destroy({ texture: true, textureSource: true }); 22824 */ 22825 destroy(options = false) { 22826 if (this.destroyed) 22827 return; 22828 this.destroyed = true; 22829 this._stateStack.length = 0; 22830 this._transform = null; 22831 this.unload(); 22832 this.emit("destroy", this); 22833 this.removeAllListeners(); 22834 const destroyTexture = typeof options === "boolean" ? options : options?.texture; 22835 if (destroyTexture) { 22836 const destroyTextureSource = typeof options === "boolean" ? options : options?.textureSource; 22837 if (this._fillStyle.texture) { 22838 this._fillStyle.fill && "uid" in this._fillStyle.fill ? this._fillStyle.fill.destroy() : this._fillStyle.texture.destroy(destroyTextureSource); 22839 } 22840 if (this._strokeStyle.texture) { 22841 this._strokeStyle.fill && "uid" in this._strokeStyle.fill ? this._strokeStyle.fill.destroy() : this._strokeStyle.texture.destroy(destroyTextureSource); 22842 } 22843 } 22844 this._fillStyle = null; 22845 this._strokeStyle = null; 22846 this.instructions = null; 22847 this._activePath = null; 22848 this._bounds = null; 22849 this._stateStack = null; 22850 this.customShader = null; 22851 this._transform = null; 22852 } 22853}; 22854/** The default fill style to use when none is provided. */ 22855_GraphicsContext.defaultFillStyle = { 22856 /** The color to use for the fill. */ 22857 color: 16777215, 22858 /** The alpha value to use for the fill. */ 22859 alpha: 1, 22860 /** The texture to use for the fill. */ 22861 texture: Texture.WHITE, 22862 /** The matrix to apply. */ 22863 matrix: null, 22864 /** The fill pattern to use. */ 22865 fill: null, 22866 /** Whether coordinates are 'global' or 'local' */ 22867 textureSpace: "local" 22868}; 22869/** The default stroke style to use when none is provided. */ 22870_GraphicsContext.defaultStrokeStyle = { 22871 /** The width of the stroke. */ 22872 width: 1, 22873 /** The color to use for the stroke. */ 22874 color: 16777215, 22875 /** The alpha value to use for the stroke. */ 22876 alpha: 1, 22877 /** The alignment of the stroke. */ 22878 alignment: 0.5, 22879 /** The miter limit to use. */ 22880 miterLimit: 10, 22881 /** The line cap style to use. */ 22882 cap: "butt", 22883 /** The line join style to use. */ 22884 join: "miter", 22885 /** The texture to use for the fill. */ 22886 texture: Texture.WHITE, 22887 /** The matrix to apply. */ 22888 matrix: null, 22889 /** The fill pattern to use. */ 22890 fill: null, 22891 /** Whether coordinates are 'global' or 'local' */ 22892 textureSpace: "local", 22893 /** If the stroke is a pixel line. */ 22894 pixelLine: false 22895}; 22896let GraphicsContext = _GraphicsContext; 22897
22898const _TextStyle = class _TextStyle extends EventEmitter { 22899 constructor(style = {}) { 22900 super(); 22901 /** 22902 * Unique identifier for the TextStyle class. 22903 * This is used to track instances and ensure uniqueness. 22904 * @internal 22905 */ 22906 this.uid = uid("textStyle"); 22907 /** 22908 * Internal tick counter used to track updates and changes. 22909 * This is incremented whenever the style is modified, allowing for efficient change detection. 22910 * @internal 22911 */ 22912 this._tick = 0; 22913 convertV7Tov8Style(style); 22914 const fullStyle = { ..._TextStyle.defaultTextStyle, ...style }; 22915 for (const key in fullStyle) { 22916 const thisKey = key; 22917 this[thisKey] = fullStyle[key]; 22918 } 22919 this.update(); 22920 this._tick = 0; 22921 } 22922 /** 22923 * Alignment for multiline text, does not affect single line text. 22924 * @type {'left'|'center'|'right'|'justify'} 22925 */ 22926 get align() { 22927 return this._align; 22928 } 22929 set align(value) { 22930 if (this._align === value) 22931 return; 22932 this._align = value; 22933 this.update(); 22934 } 22935 /** Indicates if lines can be wrapped within words, it needs wordWrap to be set to true. */ 22936 get breakWords() { 22937 return this._breakWords; 22938 } 22939 set breakWords(value) { 22940 if (this._breakWords === value) 22941 return; 22942 this._breakWords = value; 22943 this.update(); 22944 } 22945 /** Set a drop shadow for the text. */ 22946 get dropShadow() { 22947 return this._dropShadow; 22948 } 22949 set dropShadow(value) { 22950 if (this._dropShadow === value) 22951 return; 22952 if (value !== null && typeof value === "object") { 22953 this._dropShadow = this._createProxy({ ..._TextStyle.defaultDropShadow, ...value }); 22954 } else { 22955 this._dropShadow = value ? this._createProxy({ ..._TextStyle.defaultDropShadow }) : null; 22956 } 22957 this.update(); 22958 } 22959 /** The font family, can be a single font name, or a list of names where the first is the preferred font. */ 22960 get fontFamily() { 22961 return this._fontFamily; 22962 } 22963 set fontFamily(value) { 22964 if (this._fontFamily === value) 22965 return; 22966 this._fontFamily = value; 22967 this.update(); 22968 } 22969 /** The font size (as a number it converts to px, but as a string, equivalents are '26px','20pt','160%' or '1.6em') */ 22970 get fontSize() { 22971 return this._fontSize; 22972 } 22973 set fontSize(value) { 22974 if (this._fontSize === value) 22975 return; 22976 if (typeof value === "string") { 22977 this._fontSize = parseInt(value, 10); 22978 } else { 22979 this._fontSize = value; 22980 } 22981 this.update(); 22982 } 22983 /** 22984 * The font style. 22985 * @type {'normal'|'italic'|'oblique'} 22986 */ 22987 get fontStyle() { 22988 return this._fontStyle; 22989 } 22990 set fontStyle(value) { 22991 if (this._fontStyle === value) 22992 return; 22993 this._fontStyle = value.toLowerCase(); 22994 this.update(); 22995 } 22996 /** 22997 * The font variant. 22998 * @type {'normal'|'small-caps'} 22999 */ 23000 get fontVariant() { 23001 return this._fontVariant; 23002 } 23003 set fontVariant(value) { 23004 if (this._fontVariant === value) 23005 return; 23006 this._fontVariant = value; 23007 this.update(); 23008 } 23009 /** 23010 * The font weight. 23011 * @type {'normal'|'bold'|'bolder'|'lighter'|'100'|'200'|'300'|'400'|'500'|'600'|'700'|'800'|'900'} 23012 */ 23013 get fontWeight() { 23014 return this._fontWeight; 23015 } 23016 set fontWeight(value) { 23017 if (this._fontWeight === value) 23018 return; 23019 this._fontWeight = value; 23020 this.update(); 23021 } 23022 /** The space between lines. */ 23023 get leading() { 23024 return this._leading; 23025 } 23026 set leading(value) { 23027 if (this._leading === value) 23028 return; 23029 this._leading = value; 23030 this.update(); 23031 } 23032 /** The amount of spacing between letters, default is 0. */ 23033 get letterSpacing() { 23034 return this._letterSpacing; 23035 } 23036 set letterSpacing(value) { 23037 if (this._letterSpacing === value) 23038 return; 23039 this._letterSpacing = value; 23040 this.update(); 23041 } 23042 /** The line height, a number that represents the vertical space that a letter uses. */ 23043 get lineHeight() { 23044 return this._lineHeight; 23045 } 23046 set lineHeight(value) { 23047 if (this._lineHeight === value) 23048 return; 23049 this._lineHeight = value; 23050 this.update(); 23051 } 23052 /** 23053 * Occasionally some fonts are cropped. Adding some padding will prevent this from happening 23054 * by adding padding to all sides of the text. 23055 * > [!NOTE] This will NOT affect the positioning or bounds of the text. 23056 */ 23057 get padding() { 23058 return this._padding; 23059 } 23060 set padding(value) { 23061 if (this._padding === value) 23062 return; 23063 this._padding = value; 23064 this.update(); 23065 } 23066 /** 23067 * An optional filter or array of filters to apply to the text, allowing for advanced visual effects. 23068 * These filters will be applied to the text as it is created, resulting in faster rendering for static text 23069 * compared to applying the filter directly to the text object (which would be applied at run time). 23070 * @default null 23071 */ 23072 get filters() { 23073 return this._filters; 23074 } 23075 set filters(value) { 23076 if (this._filters === value) 23077 return; 23078 this._filters = Object.freeze(value); 23079 this.update(); 23080 } 23081 /** 23082 * Trim transparent borders from the text texture. 23083 * > [!IMPORTANT] PERFORMANCE WARNING: 23084 * > This is a costly operation as it requires scanning pixel alpha values. 23085 * > Avoid using `trim: true` for dynamic text, as it could significantly impact performance. 23086 */ 23087 get trim() { 23088 return this._trim; 23089 } 23090 set trim(value) { 23091 if (this._trim === value) 23092 return; 23093 this._trim = value; 23094 this.update(); 23095 } 23096 /** 23097 * The baseline of the text that is rendered. 23098 * @type {'alphabetic'|'top'|'hanging'|'middle'|'ideographic'|'bottom'} 23099 */ 23100 get textBaseline() { 23101 return this._textBaseline; 23102 } 23103 set textBaseline(value) { 23104 if (this._textBaseline === value) 23105 return; 23106 this._textBaseline = value; 23107 this.update(); 23108 } 23109 /** 23110 * How newlines and spaces should be handled. 23111 * Default is 'pre' (preserve, preserve). 23112 * 23113 * value | New lines | Spaces 23114 * --- | --- | --- 23115 * 'normal' | Collapse | Collapse 23116 * 'pre' | Preserve | Preserve 23117 * 'pre-line' | Preserve | Collapse 23118 * @type {'normal'|'pre'|'pre-line'} 23119 */ 23120 get whiteSpace() { 23121 return this._whiteSpace; 23122 } 23123 set whiteSpace(value) { 23124 if (this._whiteSpace === value) 23125 return; 23126 this._whiteSpace = value; 23127 this.update(); 23128 } 23129 /** Indicates if word wrap should be used. */ 23130 get wordWrap() { 23131 return this._wordWrap; 23132 } 23133 set wordWrap(value) { 23134 if (this._wordWrap === value) 23135 return; 23136 this._wordWrap = value; 23137 this.update(); 23138 } 23139 /** The width at which text will wrap, it needs wordWrap to be set to true. */ 23140 get wordWrapWidth() { 23141 return this._wordWrapWidth; 23142 } 23143 set wordWrapWidth(value) { 23144 if (this._wordWrapWidth === value) 23145 return; 23146 this._wordWrapWidth = value; 23147 this.update(); 23148 } 23149 /** 23150 * The fill style that will be used to color the text. 23151 * This can be: 23152 * - A color string like 'red', '#00FF00', or 'rgba(255,0,0,0.5)' 23153 * - A hex number like 0xff0000 for red 23154 * - A FillStyle object with properties like { color: 0xff0000, alpha: 0.5 } 23155 * - A FillGradient for gradient fills 23156 * - A FillPattern for pattern/texture fills 23157 * 23158 * When using a FillGradient, vertical gradients (angle of 90 degrees) are applied per line of text, 23159 * while gradients at any other angle are spread across the entire text body as a whole. 23160 * @example 23161 * // Vertical gradient applied per line 23162 * const verticalGradient = new FillGradient(0, 0, 0, 1) 23163 * .addColorStop(0, 0xff0000) 23164 * .addColorStop(1, 0x0000ff); 23165 * 23166 * const text = new Text({ 23167 * text: 'Line 1\nLine 2', 23168 * style: { fill: verticalGradient } 23169 * }); 23170 * 23171 * To manage the gradient in a global scope, set the textureSpace property of the FillGradient to 'global'. 23172 * @type {string|number|FillStyle|FillGradient|FillPattern} 23173 */ 23174 get fill() { 23175 return this._originalFill; 23176 } 23177 set fill(value) { 23178 if (value === this._originalFill) 23179 return; 23180 this._originalFill = value; 23181 if (this._isFillStyle(value)) { 23182 this._originalFill = this._createProxy({ ...GraphicsContext.defaultFillStyle, ...value }, () => { 23183 this._fill = toFillStyle( 23184 { ...this._originalFill }, 23185 GraphicsContext.defaultFillStyle 23186 ); 23187 }); 23188 } 23189 this._fill = toFillStyle( 23190 value === 0 ? "black" : value, 23191 GraphicsContext.defaultFillStyle 23192 ); 23193 this.update(); 23194 } 23195 /** A fillstyle that will be used on the text stroke, e.g., 'blue', '#FCFF00'. */ 23196 get stroke() { 23197 return this._originalStroke; 23198 } 23199 set stroke(value) { 23200 if (value === this._originalStroke) 23201 return; 23202 this._originalStroke = value; 23203 if (this._isFillStyle(value)) { 23204 this._originalStroke = this._createProxy({ ...GraphicsContext.defaultStrokeStyle, ...value }, () => { 23205 this._stroke = toStrokeStyle( 23206 { ...this._originalStroke }, 23207 GraphicsContext.defaultStrokeStyle 23208 ); 23209 }); 23210 } 23211 this._stroke = toStrokeStyle(value, GraphicsContext.defaultStrokeStyle); 23212 this.update(); 23213 } 23214 update() { 23215 this._tick++;
23216 this.emit("update", this); 23217 } 23218 /** Resets all properties to the default values */ 23219 reset() { 23220 const defaultStyle = _TextStyle.defaultTextStyle; 23221 for (const key in defaultStyle) { 23222 this[key] = defaultStyle[key]; 23223 } 23224 } 23225 /** 23226 * Returns a unique key for this instance. 23227 * This key is used for caching. 23228 * @returns {string} Unique key for the instance 23229 */ 23230 get styleKey() { 23231 return `${this.uid}-${this._tick}`; 23232 } 23233 /** 23234 * Creates a new TextStyle object with the same values as this one. 23235 * @returns New cloned TextStyle object 23236 */ 23237 clone() { 23238 return new _TextStyle({ 23239 align: this.align, 23240 breakWords: this.breakWords, 23241 dropShadow: this._dropShadow ? { ...this._dropShadow } : null, 23242 fill: this._fill, 23243 fontFamily: this.fontFamily, 23244 fontSize: this.fontSize, 23245 fontStyle: this.fontStyle, 23246 fontVariant: this.fontVariant, 23247 fontWeight: this.fontWeight, 23248 leading: this.leading, 23249 letterSpacing: this.letterSpacing, 23250 lineHeight: this.lineHeight, 23251 padding: this.padding, 23252 stroke: this._stroke, 23253 textBaseline: this.textBaseline, 23254 whiteSpace: this.whiteSpace, 23255 wordWrap: this.wordWrap, 23256 wordWrapWidth: this.wordWrapWidth, 23257 filters: this._filters ? [...this._filters] : void 0 23258 }); 23259 } 23260 /** 23261 * Returns the final padding for the text style, taking into account any filters applied. 23262 * Used internally for correct measurements 23263 * @internal 23264 * @returns {number} The final padding for the text style. 23265 */ 23266 _getFinalPadding() { 23267 let filterPadding = 0; 23268 if (this._filters) { 23269 for (let i = 0; i < this._filters.length; i++) { 23270 filterPadding += this._filters[i].padding; 23271 } 23272 } 23273 return Math.max(this._padding, filterPadding); 23274 } 23275 /** 23276 * Destroys this text style. 23277 * @param options - Options parameter. A boolean will act as if all options 23278 * have been set to that value 23279 * @example 23280 * // Destroy the text style and its textures 23281 * textStyle.destroy({ texture: true, textureSource: true }); 23282 * textStyle.destroy(true); 23283 */ 23284 destroy(options = false) { 23285 this.removeAllListeners(); 23286 const destroyTexture = typeof options === "boolean" ? options : options?.texture; 23287 if (destroyTexture) { 23288 const destroyTextureSource = typeof options === "boolean" ? options : options?.textureSource; 23289 if (this._fill?.texture) { 23290 this._fill.texture.destroy(destroyTextureSource); 23291 } 23292 if (this._originalFill?.texture) { 23293 this._originalFill.texture.destroy(destroyTextureSource); 23294 } 23295 if (this._stroke?.texture) { 23296 this._stroke.texture.destroy(destroyTextureSource); 23297 } 23298 if (this._originalStroke?.texture) { 23299 this._originalStroke.texture.destroy(destroyTextureSource); 23300 } 23301 } 23302 this._fill = null; 23303 this._stroke = null; 23304 this.dropShadow = null; 23305 this._originalStroke = null; 23306 this._originalFill = null; 23307 } 23308 _createProxy(value, cb) { 23309 return new Proxy(value, { 23310 set: (target, property, newValue) => { 23311 if (target[property] === newValue) 23312 return true; 23313 target[property] = newValue; 23314 cb?.(property, newValue); 23315 this.update(); 23316 return true; 23317 } 23318 }); 23319 } 23320 _isFillStyle(value) { 23321 return (value ?? null) !== null && !(Color.isColorLike(value) || value instanceof FillGradient || value instanceof FillPattern); 23322 } 23323}; 23324/** 23325 * Default drop shadow settings used when enabling drop shadows on text. 23326 * These values are used as the base configuration when drop shadows are enabled without specific settings. 23327 * @example 23328 * ```ts 23329 * // Customize default settings globally 23330 * TextStyle.defaultDropShadow.alpha = 0.5; // 50% opacity for all shadows 23331 * TextStyle.defaultDropShadow.blur = 2; // 2px blur for all shadows 23332 * TextStyle.defaultDropShadow.color = 'blue'; // Blue shadows by default 23333 * ``` 23334 */ 23335_TextStyle.defaultDropShadow = { 23336 alpha: 1, 23337 angle: Math.PI / 6, 23338 blur: 0, 23339 color: "black", 23340 distance: 5 23341}; 23342/** 23343 * Default text style settings used when creating new text objects. 23344 * These values serve as the base configuration and can be customized globally. 23345 * @example 23346 * ```ts 23347 * // Customize default text style globally 23348 * TextStyle.defaultTextStyle.fontSize = 16; 23349 * TextStyle.defaultTextStyle.fill = 0x333333; 23350 * TextStyle.defaultTextStyle.fontFamily = ['Arial', 'Helvetica', 'sans-serif']; 23351 * ``` 23352 */ 23353_TextStyle.defaultTextStyle = { 23354 align: "left", 23355 breakWords: false, 23356 dropShadow: null, 23357 fill: "black", 23358 fontFamily: "Arial", 23359 fontSize: 26, 23360 fontStyle: "normal", 23361 fontVariant: "normal", 23362 fontWeight: "normal", 23363 leading: 0, 23364 letterSpacing: 0, 23365 lineHeight: 0, 23366 padding: 0, 23367 stroke: null, 23368 textBaseline: "alphabetic", 23369 trim: false, 23370 whiteSpace: "pre", 23371 wordWrap: false, 23372 wordWrapWidth: 100 23373}; 23374let TextStyle = _TextStyle; 23375function convertV7Tov8Style(style) { 23376 const oldStyle = style; 23377 if (typeof oldStyle.dropShadow === "boolean" && oldStyle.dropShadow) { 23378 const defaults = TextStyle.defaultDropShadow; 23379 style.dropShadow = {
23380 alpha: oldStyle.dropShadowAlpha ?? defaults.alpha, 23381 angle: oldStyle.dropShadowAngle ?? defaults.angle, 23382 blur: oldStyle.dropShadowBlur ?? defaults.blur, 23383 color: oldStyle.dropShadowColor ?? defaults.color, 23384 distance: oldStyle.dropShadowDistance ?? defaults.distance 23385 }; 23386 } 23387 if (oldStyle.strokeThickness !== void 0) { 23388 deprecation(v8_0_0, "strokeThickness is now a part of stroke"); 23389 const color = oldStyle.stroke; 23390 let obj = {}; 23391 if (Color.isColorLike(color)) { 23392 obj.color = color; 23393 } else if (color instanceof FillGradient || color instanceof FillPattern) { 23394 obj.fill = color; 23395 } else if (Object.hasOwnProperty.call(color, "color") || Object.hasOwnProperty.call(color, "fill")) { 23396 obj = color; 23397 } else { 23398 throw new Error("Invalid stroke value."); 23399 } 23400 style.stroke = { 23401 ...obj, 23402 width: oldStyle.strokeThickness 23403 }; 23404 } 23405 if (Array.isArray(oldStyle.fillGradientStops)) { 23406 deprecation(v8_0_0, "gradient fill is now a fill pattern: `new FillGradient(...)`"); 23407 if (!Array.isArray(oldStyle.fill) || oldStyle.fill.length === 0) { 23408 throw new Error("Invalid fill value. Expected an array of colors for gradient fill."); 23409 } 23410 if (oldStyle.fill.length !== oldStyle.fillGradientStops.length) { 23411 warn("The number of fill colors must match the number of fill gradient stops."); 23412 } 23413 const gradientFill = new FillGradient({ 23414 start: { x: 0, y: 0 }, 23415 end: { x: 0, y: 1 }, 23416 textureSpace: "local" 23417 }); 23418 const fillGradientStops = oldStyle.fillGradientStops.slice(); 23419 const fills = oldStyle.fill.map((color) => Color.shared.setValue(color).toNumber()); 23420 fillGradientStops.forEach((stop, index) => { 23421 gradientFill.addColorStop(stop, fills[index]); 23422 }); 23423 style.fill = { 23424 fill: gradientFill 23425 }; 23426 } 23427} 23428 23429class CanvasPoolClass { 23430 constructor(canvasOptions) { 23431 this._canvasPool = /* @__PURE__ */ Object.create(null); 23432 this.canvasOptions = canvasOptions || {}; 23433 this.enableFullScreen = false; 23434 } 23435 /** 23436 * Creates texture with params that were specified in pool constructor. 23437 * @param pixelWidth - Width of texture in pixels. 23438 * @param pixelHeight - Height of texture in pixels. 23439 */ 23440 _createCanvasAndContext(pixelWidth, pixelHeight) { 23441 const canvas = DOMAdapter.get().createCanvas(); 23442 canvas.width = pixelWidth; 23443 canvas.height = pixelHeight; 23444 const context = canvas.getContext("2d"); 23445 return { canvas, context }; 23446 } 23447 /** 23448 * Gets a Power-of-Two render texture or fullScreen texture 23449 * @param minWidth - The minimum width of the render texture. 23450 * @param minHeight - The minimum height of the render texture. 23451 * @param resolution - The resolution of the render texture. 23452 * @returns The new render texture. 23453 */ 23454 getOptimalCanvasAndContext(minWidth, minHeight, resolution = 1) { 23455 minWidth = Math.ceil(minWidth * resolution - 1e-6); 23456 minHeight = Math.ceil(minHeight * resolution - 1e-6); 23457 minWidth = nextPow2(minWidth); 23458 minHeight = nextPow2(minHeight); 23459 const key = (minWidth << 17) + (minHeight << 1); 23460 if (!this._canvasPool[key]) { 23461 this._canvasPool[key] = []; 23462 } 23463 let canvasAndContext = this._canvasPool[key].pop(); 23464 if (!canvasAndContext) { 23465 canvasAndContext = this._createCanvasAndContext(minWidth, minHeight); 23466 } 23467 return canvasAndContext; 23468 } 23469 /** 23470 * Place a render texture back into the pool. 23471 * @param canvasAndContext 23472 */ 23473 returnCanvasAndContext(canvasAndContext) { 23474 const canvas = canvasAndContext.canvas; 23475 const { width, height } = canvas; 23476 const key = (width << 17) + (height << 1); 23477 canvasAndContext.context.resetTransform(); 23478 canvasAndContext.context.clearRect(0, 0, width, height); 23479 this._canvasPool[key].push(canvasAndContext); 23480 } 23481 clear() { 23482 this._canvasPool = {}; 23483 } 23484} 23485const CanvasPool = new CanvasPoolClass(); 23486GlobalResourceRegistry.register(CanvasPool); 23487 23488const PRECISION = 1e5; 23489function getCanvasFillStyle(fillStyle, context, textMetrics, padding = 0) { 23490 if (fillStyle.texture === Texture.WHITE && !fillStyle.fill) { 23491 return Color.shared.setValue(fillStyle.color).setAlpha(fillStyle.alpha ?? 1).toHexa(); 23492 } else if (!fillStyle.fill) { 23493 const pattern = context.createPattern(fillStyle.texture.source.resource, "repeat"); 23494 const tempMatrix = fillStyle.matrix.copyTo(Matrix.shared); 23495 tempMatrix.scale(fillStyle.texture.frame.width, fillStyle.texture.frame.height); 23496 pattern.setTransform(tempMatrix); 23497 return pattern; 23498 } else if (fillStyle.fill instanceof FillPattern) { 23499 const fillPattern = fillStyle.fill; 23500 const pattern = context.createPattern(fillPattern.texture.source.resource, "repeat"); 23501 const tempMatrix = fillPattern.transform.copyTo(Matrix.shared); 23502 tempMatrix.scale( 23503 fillPattern.texture.frame.width, 23504 fillPattern.texture.frame.height 23505 ); 23506 pattern.setTransform(tempMatrix); 23507 return pattern; 23508 } else if (fillStyle.fill instanceof FillGradient) { 23509 const fillGradient = fillStyle.fill; 23510 const isLinear = fillGradient.type === "linear"; 23511 const isLocal = fillGradient.textureSpace === "local"; 23512 let width = 1; 23513 let height = 1; 23514 if (isLocal && textMetrics) { 23515 width = textMetrics.width + padding; 23516 height = textMetrics.height + padding; 23517 } 23518 let gradient; 23519 let isNearlyVertical = false;
23520 if (isLinear) { 23521 const { start, end } = fillGradient; 23522 gradient = context.createLinearGradient( 23523 start.x * width, 23524 start.y * height, 23525 end.x * width, 23526 end.y * height 23527 ); 23528 isNearlyVertical = Math.abs(end.x - start.x) < Math.abs((end.y - start.y) * 0.1); 23529 } else { 23530 const { center, innerRadius, outerCenter, outerRadius } = fillGradient; 23531 gradient = context.createRadialGradient( 23532 center.x * width, 23533 center.y * height, 23534 innerRadius * width, 23535 outerCenter.x * width, 23536 outerCenter.y * height, 23537 outerRadius * width 23538 ); 23539 } 23540 if (isNearlyVertical && isLocal && textMetrics) { 23541 const ratio = textMetrics.lineHeight / height; 23542 for (let i = 0; i < textMetrics.lines.length; i++) { 23543 const start = (i * textMetrics.lineHeight + padding / 2) / height; 23544 fillGradient.colorStops.forEach((stop) => { 23545 const globalStop = start + stop.offset * ratio; 23546 gradient.addColorStop( 23547 // fix to 5 decimal places to avoid floating point precision issues 23548 Math.floor(globalStop * PRECISION) / PRECISION, 23549 Color.shared.setValue(stop.color).toHex() 23550 ); 23551 }); 23552 } 23553 } else { 23554 fillGradient.colorStops.forEach((stop) => { 23555 gradient.addColorStop(stop.offset, Color.shared.setValue(stop.color).toHex()); 23556 }); 23557 } 23558 return gradient; 23559 } 23560 warn("FillStyle not recognised", fillStyle); 23561 return "red"; 23562} 23563 23564const _DynamicBitmapFont = class _DynamicBitmapFont extends AbstractBitmapFont { 23565 /** 23566 * @param options - The options for the dynamic bitmap font. 23567 */ 23568 constructor(options) { 23569 super(); 23570 /** 23571 * this is a resolution modifier for the font size.. 23572 * texture resolution will also be used to scale texture according to its font size also 23573 */ 23574 this.resolution = 1; 23575 /** The pages of the font. */ 23576 this.pages = []; 23577 this._padding = 0; 23578 this._measureCache = /* @__PURE__ */ Object.create(null); 23579 this._currentChars = []; 23580 this._currentX = 0; 23581 this._currentY = 0; 23582 this._currentMaxCharHeight = 0; 23583 this._currentPageIndex = -1; 23584 this._skipKerning = false; 23585 const dynamicOptions = { ..._DynamicBitmapFont.defaultOptions, ...options }; 23586 this._textureSize = dynamicOptions.textureSize; 23587 this._mipmap = dynamicOptions.mipmap; 23588 const style = dynamicOptions.style.clone(); 23589 if (dynamicOptions.overrideFill) { 23590 style._fill.color = 16777215; 23591 style._fill.alpha = 1; 23592 style._fill.texture = Texture.WHITE; 23593 style._fill.fill = null; 23594 } 23595 this.applyFillAsTint = dynamicOptions.overrideFill; 23596 const requestedFontSize = style.fontSize; 23597 style.fontSize = this.baseMeasurementFontSize; 23598 const font = fontStringFromTextStyle(style); 23599 if (dynamicOptions.overrideSize) { 23600 if (style._stroke) { 23601 style._stroke.width *= this.baseRenderedFontSize / requestedFontSize; 23602 } 23603 } else { 23604 style.fontSize = this.baseRenderedFontSize = requestedFontSize; 23605 } 23606 this._style = style; 23607 this._skipKerning = dynamicOptions.skipKerning ?? false; 23608 this.resolution = dynamicOptions.resolution ?? 1; 23609 this._padding = dynamicOptions.padding ?? 4; 23610 if (dynamicOptions.textureStyle) { 23611 this._textureStyle = dynamicOptions.textureStyle instanceof TextureStyle ? dynamicOptions.textureStyle : new TextureStyle(dynamicOptions.textureStyle); 23612 } 23613 this.fontMetrics = CanvasTextMetrics.measureFont(font); 23614 this.lineHeight = style.lineHeight || this.fontMetrics.fontSize || style.fontSize; 23615 } 23616 ensureCharacters(chars) { 23617 const charList = CanvasTextMetrics.graphemeSegmenter(chars).filter((char) => !this._currentChars.includes(char)).filter((char, index, self) => self.indexOf(char) === index); 23618 if (!charList.length) 23619 return; 23620 this._currentChars = [...this._currentChars, ...charList]; 23621 let pageData; 23622 if (this._currentPageIndex === -1) { 23623 pageData = this._nextPage(); 23624 } else { 23625 pageData = this.pages[this._currentPageIndex]; 23626 } 23627 let { canvas, context } = pageData.canvasAndContext; 23628 let textureSource = pageData.texture.source; 23629 const style = this._style;
23630 let currentX = this._currentX; 23631 let currentY = this._currentY; 23632 let currentMaxCharHeight = this._currentMaxCharHeight; 23633 const fontScale = this.baseRenderedFontSize / this.baseMeasurementFontSize; 23634 const padding = this._padding * fontScale; 23635 let skipTexture = false; 23636 const maxTextureWidth = canvas.width / this.resolution; 23637 const maxTextureHeight = canvas.height / this.resolution; 23638 for (let i = 0; i < charList.length; i++) { 23639 const char = charList[i]; 23640 const metrics = CanvasTextMetrics.measureText(char, style, canvas, false); 23641 metrics.lineHeight = metrics.height; 23642 const width = metrics.width * fontScale; 23643 const textureGlyphWidth = Math.ceil((style.fontStyle === "italic" ? 2 : 1) * width); 23644 const height = metrics.height * fontScale; 23645 const paddedWidth = textureGlyphWidth + padding * 2; 23646 const paddedHeight = height + padding * 2; 23647 skipTexture = false; 23648 if (char !== "\n" && char !== "\r" && char !== " " && char !== " ") { 23649 skipTexture = true; 23650 currentMaxCharHeight = Math.ceil(Math.max(paddedHeight, currentMaxCharHeight)); 23651 } 23652 if (currentX + paddedWidth > maxTextureWidth) { 23653 currentY += currentMaxCharHeight; 23654 currentMaxCharHeight = paddedHeight; 23655 currentX = 0; 23656 if (currentY + currentMaxCharHeight > maxTextureHeight) { 23657 textureSource.update(); 23658 const pageData2 = this._nextPage(); 23659 canvas = pageData2.canvasAndContext.canvas; 23660 context = pageData2.canvasAndContext.context; 23661 textureSource = pageData2.texture.source; 23662 currentX = 0; 23663 currentY = 0; 23664 currentMaxCharHeight = 0; 23665 } 23666 } 23667 const xAdvance = width / fontScale - (style.dropShadow?.distance ?? 0) - (style._stroke?.width ?? 0); 23668 this.chars[char] = { 23669 id: char.codePointAt(0), 23670 xOffset: -this._padding, 23671 yOffset: -this._padding, 23672 xAdvance, 23673 kerning: {} 23674 }; 23675 if (skipTexture) { 23676 this._drawGlyph( 23677 context, 23678 metrics, 23679 currentX + padding, 23680 currentY + padding, 23681 fontScale, 23682 style 23683 ); 23684 const px = textureSource.width * fontScale; 23685 const py = textureSource.height * fontScale; 23686 const frame = new Rectangle( 23687 currentX / px * textureSource.width, 23688 currentY / py * textureSource.height, 23689 paddedWidth / px * textureSource.width, 23690 paddedHeight / py * textureSource.height 23691 ); 23692 this.chars[char].texture = new Texture({ 23693 source: textureSource, 23694 frame 23695 }); 23696 currentX += Math.ceil(paddedWidth); 23697 } 23698 } 23699 textureSource.update(); 23700 this._currentX = currentX; 23701 this._currentY = currentY; 23702 this._currentMaxCharHeight = currentMaxCharHeight; 23703 this._skipKerning && this._applyKerning(charList, context); 23704 } 23705 /** 23706 * @deprecated since 8.0.0 23707 * The map of base page textures (i.e., sheets of glyphs). 23708 */ 23709 get pageTextures() { 23710 deprecation(v8_0_0, "BitmapFont.pageTextures is deprecated, please use BitmapFont.pages instead."); 23711 return this.pages; 23712 } 23713 _applyKerning(newChars, context) { 23714 const measureCache = this._measureCache; 23715 for (let i = 0; i < newChars.length; i++) { 23716 const first = newChars[i]; 23717 for (let j = 0; j < this._currentChars.length; j++) { 23718 const second = this._currentChars[j]; 23719 let c1 = measureCache[first]; 23720 if (!c1) 23721 c1 = measureCache[first] = context.measureText(first).width; 23722 let c2 = measureCache[second]; 23723 if (!c2) 23724 c2 = measureCache[second] = context.measureText(second).width; 23725 let total = context.measureText(first + second).width; 23726 let amount = total - (c1 + c2); 23727 if (amount) { 23728 this.chars[first].kerning[second] = amount; 23729 } 23730 total = context.measureText(first + second).width; 23731 amount = total - (c1 + c2); 23732 if (amount) { 23733 this.chars[second].kerning[first] = amount; 23734 } 23735 } 23736 } 23737 } 23738 _nextPage() { 23739 this._currentPageIndex++; 23740 const textureResolution = this.resolution; 23741 const canvasAndContext = CanvasPool.getOptimalCanvasAndContext( 23742 this._textureSize, 23743 this._textureSize, 23744 textureResolution 23745 ); 23746 this._setupContext(canvasAndContext.context, this._style, textureResolution); 23747 const resolution = textureResolution * (this.baseRenderedFontSize / this.baseMeasurementFontSize); 23748 const texture = new Texture({ 23749 source: new ImageSource({ 23750 resource: canvasAndContext.canvas, 23751 resolution, 23752 alphaMode: "premultiply-alpha-on-upload", 23753 autoGenerateMipmaps: this._mipmap 23754 }) 23755 }); 23756 if (this._textureStyle) { 23757 texture.source.style = this._textureStyle; 23758 } 23759 const pageData = { 23760 canvasAndContext, 23761 texture 23762 }; 23763 this.pages[this._currentPageIndex] = pageData; 23764 return pageData; 23765 } 23766 // canvas style! 23767 _setupContext(context, style, resolution) { 23768 style.fontSize = this.baseRenderedFontSize; 23769 context.scale(resolution, resolution); 23770 context.font = fontStringFromTextStyle(style); 23771 style.fontSize = this.baseMeasurementFontSize; 23772 context.textBaseline = style.textBaseline; 23773 const stroke = style._stroke; 23774 const strokeThickness = stroke?.width ?? 0; 23775 if (stroke) { 23776 context.lineWidth = strokeThickness; 23777 context.lineJoin = stroke.join;
23778 context.miterLimit = stroke.miterLimit; 23779 context.strokeStyle = getCanvasFillStyle(stroke, context); 23780 } 23781 if (style._fill) { 23782 context.fillStyle = getCanvasFillStyle(style._fill, context); 23783 } 23784 if (style.dropShadow) { 23785 const shadowOptions = style.dropShadow; 23786 const rgb = Color.shared.setValue(shadowOptions.color).toArray(); 23787 const dropShadowBlur = shadowOptions.blur * resolution; 23788 const dropShadowDistance = shadowOptions.distance * resolution; 23789 context.shadowColor = `rgba(${rgb[0] * 255},${rgb[1] * 255},${rgb[2] * 255},${shadowOptions.alpha})`; 23790 context.shadowBlur = dropShadowBlur; 23791 context.shadowOffsetX = Math.cos(shadowOptions.angle) * dropShadowDistance; 23792 context.shadowOffsetY = Math.sin(shadowOptions.angle) * dropShadowDistance; 23793 } else { 23794 context.shadowColor = "black"; 23795 context.shadowBlur = 0; 23796 context.shadowOffsetX = 0; 23797 context.shadowOffsetY = 0; 23798 } 23799 } 23800 _drawGlyph(context, metrics, x, y, fontScale, style) { 23801 const char = metrics.text; 23802 const fontProperties = metrics.fontProperties; 23803 const stroke = style._stroke; 23804 const strokeThickness = (stroke?.width ?? 0) * fontScale; 23805 const tx = x + strokeThickness / 2; 23806 const ty = y - strokeThickness / 2; 23807 const descent = fontProperties.descent * fontScale; 23808 const lineHeight = metrics.lineHeight * fontScale; 23809 let removeShadow = false; 23810 if (style.stroke && strokeThickness) { 23811 removeShadow = true; 23812 context.strokeText(char, tx, ty + lineHeight - descent); 23813 } 23814 const { shadowBlur, shadowOffsetX, shadowOffsetY } = context; 23815 if (style._fill) { 23816 if (removeShadow) { 23817 context.shadowBlur = 0; 23818 context.shadowOffsetX = 0; 23819 context.shadowOffsetY = 0; 23820 } 23821 context.fillText(char, tx, ty + lineHeight - descent); 23822 } 23823 if (removeShadow) { 23824 context.shadowBlur = shadowBlur; 23825 context.shadowOffsetX = shadowOffsetX; 23826 context.shadowOffsetY = shadowOffsetY; 23827 } 23828 } 23829 destroy() { 23830 super.destroy(); 23831 for (let i = 0; i < this.pages.length; i++) { 23832 const { canvasAndContext, texture } = this.pages[i]; 23833 CanvasPool.returnCanvasAndContext(canvasAndContext); 23834 texture.destroy(true); 23835 } 23836 this.pages = null; 23837 } 23838}; 23839_DynamicBitmapFont.defaultOptions = { 23840 textureSize: 512, 23841 style: new TextStyle(), 23842 mipmap: true 23843}; 23844let DynamicBitmapFont = _DynamicBitmapFont; 23845 23846function getBitmapTextLayout(chars, style, font, trimEnd) { 23847 const layoutData = { 23848 width: 0, 23849 height: 0, 23850 offsetY: 0, 23851 scale: style.fontSize / font.baseMeasurementFontSize, 23852 lines: [{ 23853 width: 0, 23854 charPositions: [], 23855 spaceWidth: 0, 23856 spacesIndex: [], 23857 chars: [] 23858 }] 23859 }; 23860 layoutData.offsetY = font.baseLineOffset; 23861 let currentLine = layoutData.lines[0]; 23862 let previousChar = null; 23863 let firstWord = true; 23864 const currentWord = { 23865 width: 0, 23866 start: 0, 23867 index: 0, 23868 // use index to not modify the array as we use it a lot! 23869 positions: [], 23870 chars: [] 23871 }; 23872 const scale = font.baseMeasurementFontSize / style.fontSize; 23873 const adjustedLetterSpacing = style.letterSpacing * scale; 23874 const adjustedWordWrapWidth = style.wordWrapWidth * scale; 23875 const adjustedLineHeight = style.lineHeight ? style.lineHeight * scale : font.lineHeight; 23876 const breakWords = style.wordWrap && style.breakWords; 23877 const nextWord = (word) => { 23878 const start = currentLine.width; 23879 for (let j = 0; j < currentWord.index; j++) { 23880 const position = word.positions[j]; 23881 currentLine.chars.push(word.chars[j]); 23882 currentLine.charPositions.push(position + start); 23883 } 23884 currentLine.width += word.width; 23885 firstWord = false; 23886 currentWord.width = 0; 23887 currentWord.index = 0; 23888 currentWord.chars.length = 0; 23889 }; 23890 const nextLine = () => { 23891 let index = currentLine.chars.length - 1; 23892 if (trimEnd) { 23893 let lastChar = currentLine.chars[index]; 23894 while (lastChar === " ") { 23895 currentLine.width -= font.chars[lastChar].xAdvance; 23896 lastChar = currentLine.chars[--index]; 23897 } 23898 } 23899 layoutData.width = Math.max(layoutData.width, currentLine.width); 23900 currentLine = { 23901 width: 0, 23902 charPositions: [], 23903 chars: [], 23904 spaceWidth: 0,
23905 spacesIndex: [] 23906 }; 23907 firstWord = true; 23908 layoutData.lines.push(currentLine); 23909 layoutData.height += adjustedLineHeight; 23910 }; 23911 const checkIsOverflow = (lineWidth) => lineWidth - adjustedLetterSpacing > adjustedWordWrapWidth; 23912 for (let i = 0; i < chars.length + 1; i++) { 23913 let char; 23914 const isEnd = i === chars.length; 23915 if (!isEnd) { 23916 char = chars[i]; 23917 } 23918 const charData = font.chars[char] || font.chars[" "]; 23919 const isSpace = /(?:\s)/.test(char); 23920 const isWordBreak = isSpace || char === "\r" || char === "\n" || isEnd; 23921 if (isWordBreak) { 23922 const addWordToNextLine = !firstWord && style.wordWrap && checkIsOverflow(currentLine.width + currentWord.width); 23923 if (addWordToNextLine) { 23924 nextLine(); 23925 nextWord(currentWord); 23926 if (!isEnd) { 23927 currentLine.charPositions.push(0); 23928 } 23929 } else { 23930 currentWord.start = currentLine.width; 23931 nextWord(currentWord); 23932 if (!isEnd) { 23933 currentLine.charPositions.push(0); 23934 } 23935 } 23936 if (char === "\r" || char === "\n") { 23937 nextLine(); 23938 } else if (!isEnd) { 23939 const spaceWidth = charData.xAdvance + (charData.kerning[previousChar] || 0) + adjustedLetterSpacing; 23940 currentLine.width += spaceWidth; 23941 currentLine.spaceWidth = spaceWidth; 23942 currentLine.spacesIndex.push(currentLine.charPositions.length); 23943 currentLine.chars.push(char); 23944 } 23945 } else { 23946 const kerning = charData.kerning[previousChar] || 0; 23947 const nextCharWidth = charData.xAdvance + kerning + adjustedLetterSpacing; 23948 const addWordToNextLine = breakWords && checkIsOverflow(currentLine.width + currentWord.width + nextCharWidth); 23949 if (addWordToNextLine) { 23950 nextWord(currentWord); 23951 nextLine(); 23952 } 23953 currentWord.positions[currentWord.index++] = currentWord.width + kerning; 23954 currentWord.chars.push(char); 23955 currentWord.width += nextCharWidth; 23956 } 23957 previousChar = char; 23958 } 23959 nextLine(); 23960 if (style.align === "center") { 23961 alignCenter(layoutData); 23962 } else if (style.align === "right") { 23963 alignRight(layoutData); 23964 } else if (style.align === "justify") { 23965 alignJustify(layoutData); 23966 } 23967 return layoutData; 23968} 23969function alignCenter(measurementData) { 23970 for (let i = 0; i < measurementData.lines.length; i++) { 23971 const line = measurementData.lines[i]; 23972 const offset = measurementData.width / 2 - line.width / 2; 23973 for (let j = 0; j < line.charPositions.length; j++) { 23974 line.charPositions[j] += offset; 23975 } 23976 } 23977} 23978function alignRight(measurementData) { 23979 for (let i = 0; i < measurementData.lines.length; i++) { 23980 const line = measurementData.lines[i]; 23981 const offset = measurementData.width - line.width; 23982 for (let j = 0; j < line.charPositions.length; j++) { 23983 line.charPositions[j] += offset; 23984 } 23985 } 23986} 23987function alignJustify(measurementData) { 23988 const width = measurementData.width; 23989 for (let i = 0; i < measurementData.lines.length; i++) { 23990 const line = measurementData.lines[i]; 23991 let indy = 0; 23992 let spaceIndex = line.spacesIndex[indy++]; 23993 let offset = 0; 23994 const totalSpaces = line.spacesIndex.length; 23995 const newSpaceWidth = (width - line.width) / totalSpaces; 23996 const spaceWidth = newSpaceWidth; 23997 for (let j = 0; j < line.charPositions.length; j++) { 23998 if (j === spaceIndex) { 23999 spaceIndex = line.spacesIndex[indy++]; 24000 offset += spaceWidth; 24001 } 24002 line.charPositions[j] += offset; 24003 } 24004 } 24005} 24006 24007function resolveCharacters(chars) { 24008 if (chars === "") { 24009 return []; 24010 } 24011 if (typeof chars === "string") { 24012 chars = [chars]; 24013 } 24014 const result = []; 24015 for (let i = 0, j = chars.length; i < j; i++) { 24016 const item = chars[i]; 24017 if (Array.isArray(item)) { 24018 if (item.length !== 2) { 24019 throw new Error(`[BitmapFont]: Invalid character range length, expecting 2 got ${item.length}.`); 24020 } 24021 if (item[0].length === 0 || item[1].length === 0) { 24022 throw new Error("[BitmapFont]: Invalid character delimiter."); 24023 } 24024 const startCode = item[0].charCodeAt(0);
24025 const endCode = item[1].charCodeAt(0); 24026 if (endCode < startCode) { 24027 throw new Error("[BitmapFont]: Invalid character range."); 24028 } 24029 for (let i2 = startCode, j2 = endCode; i2 <= j2; i2++) { 24030 result.push(String.fromCharCode(i2)); 24031 } 24032 } else { 24033 result.push(...Array.from(item)); 24034 } 24035 } 24036 if (result.length === 0) { 24037 throw new Error("[BitmapFont]: Empty set when resolving characters."); 24038 } 24039 return result; 24040} 24041 24042let fontCount = 0; 24043class BitmapFontManagerClass { 24044 constructor() { 24045 /** 24046 * This character set includes all the letters in the alphabet (both lower- and upper- case). 24047 * @type {string[][]} 24048 * @example 24049 * BitmapFont.from('ExampleFont', style, { chars: BitmapFont.ALPHA }) 24050 */ 24051 this.ALPHA = [["a", "z"], ["A", "Z"], " "]; 24052 /** 24053 * This character set includes all decimal digits (from 0 to 9). 24054 * @type {string[][]} 24055 * @example 24056 * BitmapFont.from('ExampleFont', style, { chars: BitmapFont.NUMERIC }) 24057 */ 24058 this.NUMERIC = [["0", "9"]]; 24059 /** 24060 * This character set is the union of `BitmapFont.ALPHA` and `BitmapFont.NUMERIC`. 24061 * @type {string[][]} 24062 */ 24063 this.ALPHANUMERIC = [["a", "z"], ["A", "Z"], ["0", "9"], " "]; 24064 /** 24065 * This character set consists of all the ASCII table. 24066 * @type {string[][]} 24067 * @see http://www.asciitable.com/ 24068 */ 24069 this.ASCII = [[" ", "~"]]; 24070 /** Default options for installing a new BitmapFont. */ 24071 this.defaultOptions = { 24072 chars: this.ALPHANUMERIC, 24073 resolution: 1, 24074 padding: 4, 24075 skipKerning: false, 24076 textureStyle: null 24077 }; 24078 /** Cache for measured text layouts to avoid recalculating them multiple times. */ 24079 this.measureCache = lru(1e3); 24080 } 24081 /** 24082 * Get a font for the specified text and style. 24083 * @param text - The text to get the font for 24084 * @param style - The style to use 24085 */ 24086 getFont(text, style) { 24087 let fontFamilyKey = `${style.fontFamily}-bitmap`; 24088 let overrideFill = true; 24089 if (style._fill.fill && !style._stroke) { 24090 fontFamilyKey += style._fill.fill.styleKey; 24091 overrideFill = false; 24092 } else if (style._stroke || style.dropShadow) { 24093 fontFamilyKey = `${style.styleKey}-bitmap`; 24094 overrideFill = false; 24095 } 24096 if (!Cache.has(fontFamilyKey)) { 24097 const styleCopy = Object.create(style); 24098 styleCopy.lineHeight = 0; 24099 const fnt = new DynamicBitmapFont({ 24100 style: styleCopy, 24101 overrideFill, 24102 overrideSize: true, 24103 ...this.defaultOptions 24104 }); 24105 fontCount++; 24106 if (fontCount > 50) { 24107 warn("BitmapText", `You have dynamically created ${fontCount} bitmap fonts, this can be inefficient. Try pre installing your font styles using \`BitmapFont.install({name:"style1", style})\``); 24108 } 24109 fnt.once("destroy", () => { 24110 fontCount--; 24111 Cache.remove(fontFamilyKey); 24112 }); 24113 Cache.set( 24114 fontFamilyKey, 24115 fnt 24116 ); 24117 } 24118 const dynamicFont = Cache.get(fontFamilyKey); 24119 dynamicFont.ensureCharacters?.(text); 24120 return dynamicFont; 24121 } 24122 /** 24123 * Get the layout of a text for the specified style. 24124 * @param text - The text to get the layout for 24125 * @param style - The style to use 24126 * @param trimEnd - Whether to ignore whitespaces at the end of each line 24127 */ 24128 getLayout(text, style, trimEnd = true) { 24129 const bitmapFont = this.getFont(text, style); 24130 const id = `${text}-${style.styleKey}-${trimEnd}`; 24131 if (this.measureCache.has(id)) { 24132 return this.measureCache.get(id); 24133 } 24134 const segments = CanvasTextMetrics.graphemeSegmenter(text); 24135 const layoutData = getBitmapTextLayout(segments, style, bitmapFont, trimEnd); 24136 this.measureCache.set(id, layoutData); 24137 return layoutData; 24138 } 24139 /** 24140 * Measure the text using the specified style. 24141 * @param text - The text to measure 24142 * @param style - The style to use 24143 * @param trimEnd - Whether to ignore whitespaces at the end of each line 24144 */ 24145 measureText(text, style, trimEnd = true) { 24146 return this.getLayout(text, style, trimEnd); 24147 } 24148 // eslint-disable-next-line max-len 24149 install(...args) { 24150 let options = args[0]; 24151 if (typeof options === "string") { 24152 options = { 24153 name: options, 24154 style: args[1], 24155 chars: args[2]?.chars, 24156 resolution: args[2]?.resolution, 24157 padding: args[2]?.padding, 24158 skipKerning: args[2]?.skipKerning 24159 }; 24160 deprecation(v8_0_0, "BitmapFontManager.install(name, style, options) is deprecated, use BitmapFontManager.install({name, style, ...options})"); 24161 } 24162 const name = options?.name; 24163 if (!name) { 24164 throw new Error("[BitmapFontManager] Property `name` is required."); 24165 } 24166 options = { ...this.defaultOptions, ...options }; 24167 const textStyle = options.style; 24168 const style = textStyle instanceof TextStyle ? textStyle : new TextStyle(textStyle); 24169 const overrideFill = options.dynamicFill ?? this._canUseTintForStyle(style); 24170 const font = new DynamicBitmapFont({ 24171 style, 24172 overrideFill, 24173 skipKerning: options.skipKerning, 24174 padding: options.padding, 24175 resolution: options.resolution, 24176 overrideSize: false, 24177 textureStyle: options.textureStyle 24178 });
24179 const flatChars = resolveCharacters(options.chars); 24180 font.ensureCharacters(flatChars.join("")); 24181 Cache.set(`${name}-bitmap`, font); 24182 font.once("destroy", () => Cache.remove(`${name}-bitmap`)); 24183 return font; 24184 } 24185 /** 24186 * Uninstalls a bitmap font from the cache. 24187 * @param {string} name - The name of the bitmap font to uninstall. 24188 */ 24189 uninstall(name) { 24190 const cacheKey = `${name}-bitmap`; 24191 const font = Cache.get(cacheKey); 24192 if (font) { 24193 font.destroy(); 24194 } 24195 } 24196 /** 24197 * Determines if a style can use tinting instead of baking colors into the bitmap. 24198 * Tinting is more efficient as it allows reusing the same bitmap with different colors. 24199 * @param style - The text style to evaluate 24200 * @returns true if the style can use tinting, false if colors must be baked in 24201 * @private 24202 */ 24203 _canUseTintForStyle(style) { 24204 return !style._stroke && (!style.dropShadow || style.dropShadow.color === 0) && !style._fill.fill && style._fill.color === 16777215; 24205 } 24206} 24207const BitmapFontManager = new BitmapFontManagerClass(); 24208 24209class BitmapFont extends AbstractBitmapFont { 24210 constructor(options, url) { 24211 super(); 24212 const { textures, data } = options; 24213 Object.keys(data.pages).forEach((key) => { 24214 const pageData = data.pages[parseInt(key, 10)]; 24215 const texture = textures[pageData.id]; 24216 this.pages.push({ texture }); 24217 }); 24218 Object.keys(data.chars).forEach((key) => { 24219 const charData = data.chars[key]; 24220 const { 24221 frame: textureFrame, 24222 source: textureSource, 24223 rotate: textureRotate 24224 } = textures[charData.page]; 24225 const frame = groupD8.transformRectCoords( 24226 charData, 24227 textureFrame, 24228 textureRotate, 24229 new Rectangle() 24230 ); 24231 const texture = new Texture({ 24232 frame, 24233 orig: new Rectangle(0, 0, charData.width, charData.height), 24234 source: textureSource, 24235 rotate: textureRotate 24236 }); 24237 this.chars[key] = { 24238 id: key.codePointAt(0), 24239 xOffset: charData.xOffset, 24240 yOffset: charData.yOffset, 24241 xAdvance: charData.xAdvance, 24242 kerning: charData.kerning ?? {}, 24243 texture 24244 }; 24245 }); 24246 this.baseRenderedFontSize = data.fontSize; 24247 this.baseMeasurementFontSize = data.fontSize; 24248 this.fontMetrics = { 24249 ascent: 0, 24250 descent: 0, 24251 fontSize: data.fontSize 24252 }; 24253 this.baseLineOffset = data.baseLineOffset; 24254 this.lineHeight = data.lineHeight; 24255 this.fontFamily = data.fontFamily; 24256 this.distanceField = data.distanceField ?? { 24257 type: "none", 24258 range: 0 24259 }; 24260 this.url = url; 24261 } 24262 /** Destroys the BitmapFont object. */ 24263 destroy() { 24264 super.destroy(); 24265 for (let i = 0; i < this.pages.length; i++) { 24266 const { texture } = this.pages[i]; 24267 texture.destroy(true); 24268 } 24269 this.pages = null; 24270 } 24271 /** 24272 * Generates and installs a bitmap font with the specified options. 24273 * The font will be cached and available for use in BitmapText objects. 24274 * @param options - Setup options for font generation 24275 * @returns Installed font instance 24276 * @example 24277 * ```ts 24278 * // Install a basic font 24279 * BitmapFont.install({ 24280 * name: 'Title', 24281 * style: { 24282 * fontFamily: 'Arial', 24283 * fontSize: 32, 24284 * fill: '#ffffff' 24285 * } 24286 * }); 24287 * 24288 * // Install with advanced options 24289 * BitmapFont.install({ 24290 * name: 'Custom', 24291 * style: { 24292 * fontFamily: 'Arial', 24293 * fontSize: 24, 24294 * fill: '#00ff00', 24295 * stroke: { color: '#000000', width: 2 } 24296 * }, 24297 * chars: [['a', 'z'], ['A', 'Z'], ['0', '9']], 24298 * resolution: 2, 24299 * padding: 4, 24300 * textureStyle: { 24301 * scaleMode: 'nearest' 24302 * } 24303 * }); 24304 * ``` 24305 */ 24306 static install(options) { 24307 BitmapFontManager.install(options); 24308 } 24309 /** 24310 * Uninstalls a bitmap font from the cache. 24311 * This frees up memory and resources associated with the font. 24312 * @param name - The name of the bitmap font to uninstall 24313 * @example 24314 * ```ts 24315 * // Remove a font when it's no longer needed 24316 * BitmapFont.uninstall('MyCustomFont'); 24317 * 24318 * // Clear multiple fonts 24319 * ['Title', 'Heading', 'Body'].forEach(BitmapFont.uninstall); 24320 * ``` 24321 */ 24322 static uninstall(name) { 24323 BitmapFontManager.uninstall(name); 24324 } 24325} 24326 24327const bitmapFontTextParser = { 24328 test(data) { 24329 return typeof data === "string" && data.startsWith("info face="); 24330 }, 24331 parse(txt) { 24332 const items = txt.match(/^[a-z]+\s+.+$/gm); 24333 const rawData = { 24334 info: [], 24335 common: [], 24336 page: [], 24337 char: [], 24338 chars: [], 24339 kerning: [], 24340 kernings: [], 24341 distanceField: [] 24342 }; 24343 for (const i in items) { 24344 const name = items[i].match(/^[a-z]+/gm)[0]; 24345 const attributeList = items[i].match(/[a-zA-Z]+=([^\s"']+|"([^"]*)")/gm); 24346 const itemData = {}; 24347 for (const i2 in attributeList) { 24348 const split = attributeList[i2].split("=");
24349 const key = split[0]; 24350 const strValue = split[1].replace(/"/gm, ""); 24351 const floatValue = parseFloat(strValue); 24352 const value = isNaN(floatValue) ? strValue : floatValue; 24353 itemData[key] = value; 24354 } 24355 rawData[name].push(itemData); 24356 } 24357 const font = { 24358 chars: {}, 24359 pages: [], 24360 lineHeight: 0, 24361 fontSize: 0, 24362 fontFamily: "", 24363 distanceField: null, 24364 baseLineOffset: 0 24365 }; 24366 const [info] = rawData.info; 24367 const [common] = rawData.common; 24368 const [distanceField] = rawData.distanceField ?? []; 24369 if (distanceField) { 24370 font.distanceField = { 24371 range: parseInt(distanceField.distanceRange, 10), 24372 type: distanceField.fieldType 24373 }; 24374 } 24375 font.fontSize = parseInt(info.size, 10); 24376 font.fontFamily = info.face; 24377 font.lineHeight = parseInt(common.lineHeight, 10); 24378 const page = rawData.page; 24379 for (let i = 0; i < page.length; i++) { 24380 font.pages.push({ 24381 id: parseInt(page[i].id, 10) || 0, 24382 file: page[i].file 24383 }); 24384 } 24385 const map = {}; 24386 font.baseLineOffset = font.lineHeight - parseInt(common.base, 10); 24387 const char = rawData.char; 24388 for (let i = 0; i < char.length; i++) { 24389 const charNode = char[i]; 24390 const id = parseInt(charNode.id, 10); 24391 let letter = charNode.letter ?? charNode.char ?? String.fromCharCode(id); 24392 if (letter === "space") 24393 letter = " "; 24394 map[id] = letter; 24395 font.chars[letter] = { 24396 id, 24397 // texture deets.. 24398 page: parseInt(charNode.page, 10) || 0, 24399 x: parseInt(charNode.x, 10), 24400 y: parseInt(charNode.y, 10), 24401 width: parseInt(charNode.width, 10), 24402 height: parseInt(charNode.height, 10), 24403 xOffset: parseInt(charNode.xoffset, 10), 24404 yOffset: parseInt(charNode.yoffset, 10), 24405 xAdvance: parseInt(charNode.xadvance, 10), 24406 kerning: {} 24407 }; 24408 } 24409 const kerning = rawData.kerning || []; 24410 for (let i = 0; i < kerning.length; i++) { 24411 const first = parseInt(kerning[i].first, 10); 24412 const second = parseInt(kerning[i].second, 10); 24413 const amount = parseInt(kerning[i].amount, 10); 24414 font.chars[map[second]].kerning[map[first]] = amount; 24415 } 24416 return font; 24417 } 24418}; 24419 24420const bitmapFontXMLParser = { 24421 test(data) { 24422 const xml = data; 24423 return typeof xml !== "string" && "getElementsByTagName" in xml && xml.getElementsByTagName("page").length && xml.getElementsByTagName("info")[0].getAttribute("face") !== null; 24424 }, 24425 parse(xml) { 24426 const data = { 24427 chars: {}, 24428 pages: [], 24429 lineHeight: 0, 24430 fontSize: 0, 24431 fontFamily: "", 24432 distanceField: null, 24433 baseLineOffset: 0 24434 }; 24435 const info = xml.getElementsByTagName("info")[0]; 24436 const common = xml.getElementsByTagName("common")[0]; 24437 const distanceField = xml.getElementsByTagName("distanceField")[0]; 24438 if (distanceField) { 24439 data.distanceField = { 24440 type: distanceField.getAttribute("fieldType"), 24441 range: parseInt(distanceField.getAttribute("distanceRange"), 10) 24442 }; 24443 } 24444 const page = xml.getElementsByTagName("page"); 24445 const char = xml.getElementsByTagName("char"); 24446 const kerning = xml.getElementsByTagName("kerning"); 24447 data.fontSize = parseInt(info.getAttribute("size"), 10); 24448 data.fontFamily = info.getAttribute("face"); 24449 data.lineHeight = parseInt(common.getAttribute("lineHeight"), 10); 24450 for (let i = 0; i < page.length; i++) { 24451 data.pages.push({ 24452 id: parseInt(page[i].getAttribute("id"), 10) || 0, 24453 file: page[i].getAttribute("file") 24454 }); 24455 } 24456 const map = {}; 24457 data.baseLineOffset = data.lineHeight - parseInt(common.getAttribute("base"), 10); 24458 for (let i = 0; i < char.length; i++) { 24459 const charNode = char[i]; 24460 const id = parseInt(charNode.getAttribute("id"), 10); 24461 let letter = charNode.getAttribute("letter") ?? charNode.getAttribute("char") ?? String.fromCharCode(id); 24462 if (letter === "space") 24463 letter = " "; 24464 map[id] = letter; 24465 data.chars[letter] = { 24466 id, 24467 // texture deets.. 24468 page: parseInt(charNode.getAttribute("page"), 10) || 0, 24469 x: parseInt(charNode.getAttribute("x"), 10), 24470 y: parseInt(charNode.getAttribute("y"), 10), 24471 width: parseInt(charNode.getAttribute("width"), 10), 24472 height: parseInt(charNode.getAttribute("height"), 10), 24473 // render deets.. 24474 xOffset: parseInt(charNode.getAttribute("xoffset"), 10), 24475 yOffset: parseInt(charNode.getAttribute("yoffset"), 10), 24476 // + baseLineOffset, 24477 xAdvance: parseInt(charNode.getAttribute("xadvance"), 10), 24478 kerning: {} 24479 }; 24480 } 24481 for (let i = 0; i < kerning.length; i++) { 24482 const first = parseInt(kerning[i].getAttribute("first"), 10); 24483 const second = parseInt(kerning[i].getAttribute("second"), 10); 24484 const amount = parseInt(kerning[i].getAttribute("amount"), 10); 24485 data.chars[map[second]].kerning[map[first]] = amount; 24486 } 24487 return data; 24488 } 24489}; 24490 24491const bitmapFontXMLStringParser = { 24492 test(data) { 24493 if (typeof data === "string" && data.match(/<font(\s|>)/)) { 24494 return bitmapFontXMLParser.test(DOMAdapter.get().parseXML(data)); 24495 } 24496 return false;
24497 }, 24498 parse(data) { 24499 return bitmapFontXMLParser.parse(DOMAdapter.get().parseXML(data)); 24500 } 24501}; 24502 24503const validExtensions = [".xml", ".fnt"]; 24504const bitmapFontCachePlugin = { 24505 extension: { 24506 type: ExtensionType.CacheParser, 24507 name: "cacheBitmapFont" 24508 }, 24509 test: (asset) => asset instanceof BitmapFont, 24510 getCacheableAssets(keys, asset) { 24511 const out = {}; 24512 keys.forEach((key) => { 24513 out[key] = asset; 24514 out[`${key}-bitmap`] = asset; 24515 }); 24516 out[`${asset.fontFamily}-bitmap`] = asset; 24517 return out; 24518 } 24519}; 24520const loadBitmapFont = { 24521 extension: { 24522 type: ExtensionType.LoadParser, 24523 priority: LoaderParserPriority.Normal 24524 }, 24525 /** used for deprecation purposes */ 24526 name: "loadBitmapFont", 24527 id: "bitmap-font", 24528 test(url) { 24529 return validExtensions.includes(path.extname(url).toLowerCase()); 24530 }, 24531 async testParse(data) { 24532 return bitmapFontTextParser.test(data) || bitmapFontXMLStringParser.test(data); 24533 }, 24534 async parse(asset, data, loader) { 24535 const bitmapFontData = bitmapFontTextParser.test(asset) ? bitmapFontTextParser.parse(asset) : bitmapFontXMLStringParser.parse(asset); 24536 const { src } = data; 24537 const { pages } = bitmapFontData; 24538 const textureUrls = []; 24539 const textureOptions = bitmapFontData.distanceField ? { 24540 scaleMode: "linear", 24541 alphaMode: "premultiply-alpha-on-upload", 24542 autoGenerateMipmaps: false, 24543 resolution: 1 24544 } : {}; 24545 for (let i = 0; i < pages.length; ++i) { 24546 const pageFile = pages[i].file; 24547 let imagePath = path.join(path.dirname(src), pageFile); 24548 imagePath = copySearchParams(imagePath, src); 24549 textureUrls.push({ 24550 src: imagePath, 24551 data: textureOptions 24552 }); 24553 } 24554 const loadedTextures = await loader.load(textureUrls); 24555 const textures = textureUrls.map((url) => loadedTextures[url.src]); 24556 const bitmapFont = new BitmapFont({ 24557 data: bitmapFontData, 24558 textures 24559 }, src); 24560 return bitmapFont; 24561 }, 24562 async load(url, _options) { 24563 const response = await DOMAdapter.get().fetch(url); 24564 return await response.text(); 24565 }, 24566 async unload(bitmapFont, _resolvedAsset, loader) { 24567 await Promise.all(bitmapFont.pages.map((page) => loader.unload(page.texture.source._sourceOrigin))); 24568 bitmapFont.destroy(); 24569 } 24570}; 24571 24572class BackgroundLoader { 24573 /** 24574 * @param loader 24575 * @param verbose - should the loader log to the console 24576 */ 24577 constructor(loader, verbose = false) { 24578 this._loader = loader; 24579 this._assetList = []; 24580 this._isLoading = false; 24581 this._maxConcurrent = 1; 24582 this.verbose = verbose; 24583 } 24584 /** 24585 * Adds assets to the background loading queue. Assets are loaded one at a time to minimize 24586 * performance impact. 24587 * @param assetUrls - Array of resolved assets to load in the background 24588 * @example 24589 * ```ts 24590 * // Add assets to background load queue 24591 * backgroundLoader.add([ 24592 * { src: 'images/level1/bg.png' }, 24593 * { src: 'images/level1/characters.json' } 24594 * ]); 24595 * 24596 * // Assets will load sequentially in the background 24597 * // The loader automatically pauses when high-priority loads occur 24598 * // e.g. Assets.load() is called 24599 * ``` 24600 * @remarks 24601 * - Assets are loaded one at a time to minimize performance impact 24602 * - Loading automatically pauses when Assets.load() is called 24603 * - No progress tracking is available for background loading 24604 * - Assets are cached as they complete loading 24605 * @internal 24606 */ 24607 add(assetUrls) {
24608 assetUrls.forEach((a) => { 24609 this._assetList.push(a); 24610 }); 24611 if (this.verbose) { 24612 console.log("[BackgroundLoader] assets: ", this._assetList); 24613 } 24614 if (this._isActive && !this._isLoading) { 24615 void this._next(); 24616 } 24617 } 24618 /** 24619 * Loads the next set of assets. Will try to load as many assets as it can at the same time. 24620 * 24621 * The max assets it will try to load at one time will be 4. 24622 */ 24623 async _next() { 24624 if (this._assetList.length && this._isActive) { 24625 this._isLoading = true; 24626 const toLoad = []; 24627 const toLoadAmount = Math.min(this._assetList.length, this._maxConcurrent); 24628 for (let i = 0; i < toLoadAmount; i++) { 24629 toLoad.push(this._assetList.pop()); 24630 } 24631 await this._loader.load(toLoad); 24632 this._isLoading = false; 24633 void this._next(); 24634 } 24635 } 24636 /** 24637 * Controls the active state of the background loader. When active, the loader will 24638 * continue processing its queue. When inactive, loading is paused. 24639 * @returns Whether the background loader is currently active 24640 * @example 24641 * ```ts 24642 * // Pause background loading 24643 * backgroundLoader.active = false; 24644 * 24645 * // Resume background loading 24646 * backgroundLoader.active = true; 24647 * 24648 * // Check current state 24649 * console.log(backgroundLoader.active); // true/false 24650 * 24651 * // Common use case: Pause during intensive operations 24652 * backgroundLoader.active = false; // Pause background loading 24653 * ... // Perform high-priority tasks 24654 * backgroundLoader.active = true; // Resume background loading 24655 * ``` 24656 * @remarks 24657 * - Setting to true resumes loading immediately 24658 * - Setting to false pauses after current asset completes 24659 * - Background loading is automatically paused during `Assets.load()` 24660 * - Assets already being loaded will complete even when set to false 24661 */ 24662 get active() { 24663 return this._isActive; 24664 } 24665 set active(value) { 24666 if (this._isActive === value) 24667 return; 24668 this._isActive = value; 24669 if (value && !this._isLoading) { 24670 void this._next(); 24671 } 24672 } 24673} 24674 24675const cacheTextureArray = { 24676 extension: { 24677 type: ExtensionType.CacheParser, 24678 name: "cacheTextureArray" 24679 }, 24680 test: (asset) => Array.isArray(asset) && asset.every((t) => t instanceof Texture), 24681 getCacheableAssets: (keys, asset) => { 24682 const out = {}; 24683 keys.forEach((key) => { 24684 asset.forEach((item, i) => { 24685 out[key + (i === 0 ? "" : i + 1)] = item; 24686 }); 24687 }); 24688 return out; 24689 } 24690}; 24691 24692async function testImageFormat(imageData) { 24693 if ("Image" in globalThis) { 24694 return new Promise((resolve) => { 24695 const image = new Image(); 24696 image.onload = () => { 24697 resolve(true); 24698 }; 24699 image.onerror = () => { 24700 resolve(false); 24701 }; 24702 image.src = imageData; 24703 }); 24704 } 24705 if ("createImageBitmap" in globalThis && "fetch" in globalThis) { 24706 try { 24707 const blob = await (await fetch(imageData)).blob(); 24708 await createImageBitmap(blob); 24709 } catch (_e) { 24710 return false; 24711 } 24712 return true; 24713 } 24714 return false; 24715} 24716 24717const detectAvif = { 24718 extension: { 24719 type: ExtensionType.DetectionParser, 24720 priority: 1 24721 }, 24722 test: async () => testImageFormat( 24723 // eslint-disable-next-line max-len 24724 "data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=" 24725 ), 24726 add: async (formats) => [...formats, "avif"], 24727 remove: async (formats) => formats.filter((f) => f !== "avif") 24728}; 24729 24730const imageFormats = ["png", "jpg", "jpeg"]; 24731const detectDefaults = { 24732 extension: { 24733 type: ExtensionType.DetectionParser, 24734 priority: -1 24735 }, 24736 test: () => Promise.resolve(true), 24737 add: async (formats) => [...formats, ...imageFormats], 24738 remove: async (formats) => formats.filter((f) => !imageFormats.includes(f)) 24739}; 24740 24741const inWorker = "WorkerGlobalScope" in globalThis && globalThis instanceof globalThis.WorkerGlobalScope; 24742function testVideoFormat(mimeType) { 24743 if (inWorker) { 24744 return false;
24745 } 24746 const video = document.createElement("video"); 24747 return video.canPlayType(mimeType) !== ""; 24748} 24749 24750const detectMp4 = { 24751 extension: { 24752 type: ExtensionType.DetectionParser, 24753 priority: 0 24754 }, 24755 test: async () => testVideoFormat("video/mp4"), 24756 add: async (formats) => [...formats, "mp4", "m4v"], 24757 remove: async (formats) => formats.filter((f) => f !== "mp4" && f !== "m4v") 24758}; 24759 24760const detectOgv = { 24761 extension: { 24762 type: ExtensionType.DetectionParser, 24763 priority: 0 24764 }, 24765 test: async () => testVideoFormat("video/ogg"), 24766 add: async (formats) => [...formats, "ogv"], 24767 remove: async (formats) => formats.filter((f) => f !== "ogv") 24768}; 24769 24770const detectWebm = { 24771 extension: { 24772 type: ExtensionType.DetectionParser, 24773 priority: 0 24774 }, 24775 test: async () => testVideoFormat("video/webm"), 24776 add: async (formats) => [...formats, "webm"], 24777 remove: async (formats) => formats.filter((f) => f !== "webm") 24778}; 24779 24780const detectWebp = { 24781 extension: { 24782 type: ExtensionType.DetectionParser, 24783 priority: 0 24784 }, 24785 test: async () => testImageFormat( 24786 "data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=" 24787 ), 24788 add: async (formats) => [...formats, "webp"], 24789 remove: async (formats) => formats.filter((f) => f !== "webp") 24790}; 24791 24792const _Loader = class _Loader { 24793 constructor() { 24794 /** 24795 * Options for loading assets with the loader. 24796 * These options will be used as defaults for all load calls made with this loader instance. 24797 * They can be overridden by passing options directly to the load method. 24798 * @example 24799 * ```ts 24800 * // Create a loader with custom default options 24801 * const loader = new Loader(); 24802 * loader.loadOptions = { 24803 * strategy: 'skip', // Default strategy to 'skip' 24804 * retryCount: 5, // Default retry count to 5 24805 * retryDelay: 500, // Default retry delay to 500ms 24806 * }; 24807 * 24808 * // This load call will use the loader's default options 24809 * await loader.load('image1.png'); 24810 */ 24811 this.loadOptions = { ..._Loader.defaultOptions }; 24812 this._parsers = []; 24813 this._parsersValidated = false; 24814 /** 24815 * All loader parsers registered 24816 * @type {assets.LoaderParser[]} 24817 */ 24818 this.parsers = new Proxy(this._parsers, { 24819 set: (target, key, value) => { 24820 this._parsersValidated = false; 24821 target[key] = value; 24822 return true; 24823 } 24824 }); 24825 /** Cache loading promises that ae currently active */ 24826 this.promiseCache = {}; 24827 } 24828 /** function used for testing */ 24829 reset() { 24830 this._parsersValidated = false; 24831 this.promiseCache = {}; 24832 } 24833 /** 24834 * Used internally to generate a promise for the asset to be loaded. 24835 * @param url - The URL to be loaded 24836 * @param data - any custom additional information relevant to the asset being loaded 24837 * @returns - a promise that will resolve to an Asset for example a Texture of a JSON object 24838 */ 24839 _getLoadPromiseAndParser(url, data) { 24840 const result = { 24841 promise: null, 24842 parser: null 24843 }; 24844 result.promise = (async () => { 24845 let asset = null; 24846 let parser = null; 24847 if (data.parser || data.loadParser) { 24848 parser = this._parserHash[data.parser || data.loadParser]; 24849 if (data.loadParser) { 24850 warn( 24851 `[Assets] "loadParser" is deprecated, use "parser" instead for ${url}` 24852 ); 24853 } 24854 if (!parser) { 24855 warn( 24856 `[Assets] specified load parser "${data.parser || data.loadParser}" not found while loading ${url}` 24857 ); 24858 } 24859 } 24860 if (!parser) { 24861 for (let i = 0; i < this.parsers.length; i++) { 24862 const parserX = this.parsers[i]; 24863 if (parserX.load && parserX.test?.(url, data, this)) { 24864 parser = parserX; 24865 break; 24866 } 24867 } 24868 if (!parser) { 24869 warn(`[Assets] ${url} could not be loaded as we don't know how to parse it, ensure the correct parser has been added`); 24870 return null; 24871 } 24872 } 24873 asset = await parser.load(url, data, this); 24874 result.parser = parser; 24875 for (let i = 0; i < this.parsers.length; i++) { 24876 const parser2 = this.parsers[i]; 24877 if (parser2.parse) { 24878 if (parser2.parse && await parser2.testParse?.(asset, data, this)) { 24879 asset = await parser2.parse(asset, data, this) || asset; 24880 result.parser = parser2; 24881 } 24882 } 24883 } 24884 return asset; 24885 })(); 24886 return result; 24887 } 24888 async load(assetsToLoadIn, onProgressOrOptions) { 24889 if (!this._parsersValidated) {
24890 this._validateParsers(); 24891 } 24892 const options = typeof onProgressOrOptions === "function" ? { ..._Loader.defaultOptions, ...this.loadOptions, onProgress: onProgressOrOptions } : { ..._Loader.defaultOptions, ...this.loadOptions, ...onProgressOrOptions || {} }; 24893 const { onProgress, onError, strategy, retryCount, retryDelay } = options; 24894 let count = 0; 24895 const assets = {}; 24896 const singleAsset = isSingleItem(assetsToLoadIn); 24897 const assetsToLoad = convertToList(assetsToLoadIn, (item) => ({ 24898 alias: [item], 24899 src: item, 24900 data: {} 24901 })); 24902 const total = assetsToLoad.reduce((sum, asset) => sum + (asset.progressSize || 1), 0); 24903 const promises = assetsToLoad.map(async (asset) => { 24904 const url = path.toAbsolute(asset.src); 24905 if (assets[asset.src]) 24906 return; 24907 await this._loadAssetWithRetry(url, asset, { onProgress, onError, strategy, retryCount, retryDelay }, assets); 24908 count += asset.progressSize || 1; 24909 if (onProgress) 24910 onProgress(count / total); 24911 }); 24912 await Promise.all(promises); 24913 return singleAsset ? assets[assetsToLoad[0].src] : assets; 24914 } 24915 /** 24916 * Unloads one or more assets. Any unloaded assets will be destroyed, freeing up memory for your app. 24917 * The parser that created the asset, will be the one that unloads it. 24918 * @example 24919 * // Single asset: 24920 * const asset = await Loader.load('cool.png'); 24921 * 24922 * await Loader.unload('cool.png'); 24923 * 24924 * console.log(asset.destroyed); // true 24925 * @param assetsToUnloadIn - urls that you want to unload, or a single one! 24926 */ 24927 async unload(assetsToUnloadIn) { 24928 const assetsToUnload = convertToList(assetsToUnloadIn, (item) => ({ 24929 alias: [item], 24930 src: item 24931 })); 24932 const promises = assetsToUnload.map(async (asset) => { 24933 const url = path.toAbsolute(asset.src); 24934 const loadPromise = this.promiseCache[url]; 24935 if (loadPromise) { 24936 const loadedAsset = await loadPromise.promise; 24937 delete this.promiseCache[url]; 24938 await loadPromise.parser?.unload?.(loadedAsset, asset, this); 24939 } 24940 }); 24941 await Promise.all(promises); 24942 } 24943 /** validates our parsers, right now it only checks for name conflicts but we can add more here as required! */ 24944 _validateParsers() { 24945 this._parsersValidated = true; 24946 this._parserHash = this._parsers.filter((parser) => parser.name || parser.id).reduce((hash, parser) => { 24947 if (!parser.name && !parser.id) { 24948 warn(`[Assets] parser should have an id`); 24949 } else if (hash[parser.name] || hash[parser.id]) { 24950 warn(`[Assets] parser id conflict "${parser.id}"`); 24951 } 24952 hash[parser.name] = parser; 24953 if (parser.id) 24954 hash[parser.id] = parser; 24955 return hash; 24956 }, {}); 24957 } 24958 async _loadAssetWithRetry(url, asset, options, assets) { 24959 let attempt = 0; 24960 const { onError, strategy, retryCount, retryDelay } = options; 24961 const wait = (ms) => new Promise((r) => setTimeout(r, ms)); 24962 while (true) { 24963 try { 24964 if (!this.promiseCache[url]) { 24965 this.promiseCache[url] = this._getLoadPromiseAndParser(url, asset); 24966 } 24967 assets[asset.src] = await this.promiseCache[url].promise; 24968 return; 24969 } catch (e) { 24970 delete this.promiseCache[url]; 24971 delete assets[asset.src]; 24972 attempt++; 24973 const isLast = strategy !== "retry" || attempt > retryCount; 24974 if (strategy === "retry" && !isLast) { 24975 if (onError) 24976 onError(e, asset); 24977 await wait(retryDelay); 24978 continue; 24979 } 24980 if (strategy === "skip") { 24981 if (onError) 24982 onError(e, asset); 24983 return; 24984 } 24985 if (onError) 24986 onError(e, asset); 24987 const error = new Error(`[Loader.load] Failed to load ${url}. 24988${e}`); 24989 if (e instanceof Error && e.stack) { 24990 error.stack = e.stack; 24991 } 24992 throw error; 24993 } 24994 } 24995 } 24996}; 24997/** 24998 * Default options for loading assets 24999 * @example 25000 * ```ts 25001 * // Change default load options globally 25002 * Loader.defaultOptions = { 25003 * strategy: 'skip', // Change default strategy to 'skip' 25004 * retryCount: 5, // Change default retry count to 5 25005 * retryDelay: 500, // Change default retry delay to 500ms 25006 * }; 25007 * ``` 25008 */ 25009_Loader.defaultOptions = { 25010 onProgress: void 0, 25011 onError: void 0, 25012 strategy: "throw", 25013 retryCount: 3, 25014 retryDelay: 250 25015}; 25016let Loader = _Loader; 25017 25018function checkDataUrl(url, mimes) { 25019 if (Array.isArray(mimes)) { 25020 for (const mime of mimes) { 25021 if (url.startsWith(`data:${mime}`)) 25022 return true; 25023 } 25024 return false;
25025 } 25026 return url.startsWith(`data:${mimes}`); 25027} 25028 25029function checkExtension(url, extension) { 25030 const tempURL = url.split("?")[0]; 25031 const ext = path.extname(tempURL).toLowerCase(); 25032 if (Array.isArray(extension)) { 25033 return extension.includes(ext); 25034 } 25035 return ext === extension; 25036} 25037 25038const validJSONExtension = ".json"; 25039const validJSONMIME = "application/json"; 25040const loadJson = { 25041 extension: { 25042 type: ExtensionType.LoadParser, 25043 priority: LoaderParserPriority.Low 25044 }, 25045 /** used for deprecation purposes */ 25046 name: "loadJson", 25047 id: "json", 25048 test(url) { 25049 return checkDataUrl(url, validJSONMIME) || checkExtension(url, validJSONExtension); 25050 }, 25051 async load(url) { 25052 const response = await DOMAdapter.get().fetch(url); 25053 const json = await response.json(); 25054 return json; 25055 } 25056}; 25057 25058const validTXTExtension = ".txt"; 25059const validTXTMIME = "text/plain"; 25060const loadTxt = { 25061 /** used for deprecation purposes */ 25062 name: "loadTxt", 25063 id: "text", 25064 extension: { 25065 type: ExtensionType.LoadParser, 25066 priority: LoaderParserPriority.Low, 25067 name: "loadTxt" 25068 }, 25069 test(url) { 25070 return checkDataUrl(url, validTXTMIME) || checkExtension(url, validTXTExtension); 25071 }, 25072 async load(url) { 25073 const response = await DOMAdapter.get().fetch(url); 25074 const txt = await response.text(); 25075 return txt; 25076 } 25077}; 25078 25079const validWeights = [ 25080 "normal", 25081 "bold", 25082 "100", 25083 "200", 25084 "300", 25085 "400", 25086 "500", 25087 "600", 25088 "700", 25089 "800", 25090 "900" 25091]; 25092const validFontExtensions = [".ttf", ".otf", ".woff", ".woff2"]; 25093const validFontMIMEs = [ 25094 "font/ttf", 25095 "font/otf", 25096 "font/woff", 25097 "font/woff2" 25098]; 25099const CSS_IDENT_TOKEN_REGEX = /^(--|-?[A-Z_])[0-9A-Z_-]*$/i; 25100function getFontFamilyName(url) { 25101 const ext = path.extname(url); 25102 const name = path.basename(url, ext); 25103 const nameWithSpaces = name.replace(/(-|_)/g, " "); 25104 const nameTokens = nameWithSpaces.toLowerCase().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)); 25105 let valid = nameTokens.length > 0; 25106 for (const token of nameTokens) { 25107 if (!token.match(CSS_IDENT_TOKEN_REGEX)) { 25108 valid = false; 25109 break; 25110 } 25111 } 25112 let fontFamilyName = nameTokens.join(" "); 25113 if (!valid) { 25114 fontFamilyName = `"${fontFamilyName.replace(/[\\"]/g, "\\$&")}"`; 25115 } 25116 return fontFamilyName; 25117} 25118const validURICharactersRegex = /^[0-9A-Za-z%:/?#\[\]@!\$&'()\*\+,;=\-._~]*$/; 25119function encodeURIWhenNeeded(uri) { 25120 if (validURICharactersRegex.test(uri)) { 25121 return uri; 25122 } 25123 return encodeURI(uri); 25124} 25125const loadWebFont = { 25126 extension: { 25127 type: ExtensionType.LoadParser, 25128 priority: LoaderParserPriority.Low 25129 }, 25130 /** used for deprecation purposes */ 25131 name: "loadWebFont", 25132 id: "web-font", 25133 test(url) { 25134 return checkDataUrl(url, validFontMIMEs) || checkExtension(url, validFontExtensions); 25135 }, 25136 async load(url, options) { 25137 const fonts = DOMAdapter.get().getFontFaceSet(); 25138 if (fonts) { 25139 const fontFaces = []; 25140 const name = options.data?.family ?? getFontFamilyName(url); 25141 const weights = options.data?.weights?.filter((weight) => validWeights.includes(weight)) ?? ["normal"]; 25142 const data = options.data ?? {}; 25143 for (let i = 0; i < weights.length; i++) { 25144 const weight = weights[i]; 25145 const font = new FontFace(name, `url(${encodeURIWhenNeeded(url)})`, { 25146 ...data, 25147 weight 25148 }); 25149 await font.load(); 25150 fonts.add(font); 25151 fontFaces.push(font); 25152 } 25153 if (Cache.has(`${name}-and-url`)) { 25154 const cached = Cache.get(`${name}-and-url`); 25155 cached.entries.push({ url, faces: fontFaces }); 25156 } else { 25157 Cache.set(`${name}-and-url`, { 25158 entries: [{ url, faces: fontFaces }] 25159 }); 25160 } 25161 return fontFaces.length === 1 ? fontFaces[0] : fontFaces; 25162 } 25163 warn("[loadWebFont] FontFace API is not supported. Skipping loading font"); 25164 return null; 25165 }, 25166 unload(font) { 25167 const fonts = Array.isArray(font) ? font : [font]; 25168 const fontFamily = fonts[0].family; 25169 const cached = Cache.get(`${fontFamily}-and-url`); 25170 const entry = cached.entries.find((f) => f.faces.some((t) => fonts.indexOf(t) !== -1)); 25171 entry.faces = entry.faces.filter((f) => fonts.indexOf(f) === -1); 25172 if (entry.faces.length === 0) { 25173 cached.entries = cached.entries.filter((f) => f !== entry); 25174 }
25175 fonts.forEach((t) => { 25176 DOMAdapter.get().getFontFaceSet().delete(t); 25177 }); 25178 if (cached.entries.length === 0) { 25179 Cache.remove(`${fontFamily}-and-url`); 25180 } 25181 } 25182}; 25183 25184function getResolutionOfUrl(url, defaultValue = 1) { 25185 const resolution = Resolver.RETINA_PREFIX?.exec(url); 25186 if (resolution) { 25187 return parseFloat(resolution[1]); 25188 } 25189 return defaultValue; 25190} 25191 25192function createTexture(source, loader, url) { 25193 source.label = url; 25194 source._sourceOrigin = url; 25195 const texture = new Texture({ 25196 source, 25197 label: url 25198 }); 25199 const unload = () => { 25200 delete loader.promiseCache[url]; 25201 if (Cache.has(url)) { 25202 Cache.remove(url); 25203 } 25204 }; 25205 texture.source.once("destroy", () => { 25206 if (loader.promiseCache[url]) { 25207 warn("[Assets] A TextureSource managed by Assets was destroyed instead of unloaded! Use Assets.unload() instead of destroying the TextureSource."); 25208 unload(); 25209 } 25210 }); 25211 texture.once("destroy", () => { 25212 if (!source.destroyed) { 25213 warn("[Assets] A Texture managed by Assets was destroyed instead of unloaded! Use Assets.unload() instead of destroying the Texture."); 25214 unload(); 25215 } 25216 }); 25217 return texture; 25218} 25219 25220const validSVGExtension = ".svg"; 25221const validSVGMIME = "image/svg+xml"; 25222const loadSvg = { 25223 extension: { 25224 type: ExtensionType.LoadParser, 25225 priority: LoaderParserPriority.Low, 25226 name: "loadSVG" 25227 }, 25228 /** used for deprecation purposes */ 25229 name: "loadSVG", 25230 id: "svg", 25231 config: { 25232 crossOrigin: "anonymous", 25233 parseAsGraphicsContext: false 25234 }, 25235 test(url) { 25236 return checkDataUrl(url, validSVGMIME) || checkExtension(url, validSVGExtension); 25237 }, 25238 async load(url, asset, loader) { 25239 if (asset.data?.parseAsGraphicsContext ?? this.config.parseAsGraphicsContext) { 25240 return loadAsGraphics(url); 25241 } 25242 return loadAsTexture(url, asset, loader, this.config.crossOrigin); 25243 }, 25244 unload(asset) { 25245 asset.destroy(true); 25246 } 25247}; 25248async function loadAsTexture(url, asset, loader, crossOrigin) { 25249 const response = await DOMAdapter.get().fetch(url); 25250 const image = DOMAdapter.get().createImage(); 25251 image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(await response.text())}`; 25252 image.crossOrigin = crossOrigin; 25253 await image.decode(); 25254 const width = asset.data?.width ?? image.width; 25255 const height = asset.data?.height ?? image.height; 25256 const resolution = asset.data?.resolution || getResolutionOfUrl(url); 25257 const canvasWidth = Math.ceil(width * resolution); 25258 const canvasHeight = Math.ceil(height * resolution); 25259 const canvas = DOMAdapter.get().createCanvas(canvasWidth, canvasHeight); 25260 const context = canvas.getContext("2d"); 25261 context.imageSmoothingEnabled = true; 25262 context.imageSmoothingQuality = "high"; 25263 context.drawImage(image, 0, 0, width * resolution, height * resolution); 25264 const { parseAsGraphicsContext: _p, ...rest } = asset.data ?? {}; 25265 const base = new ImageSource({ 25266 resource: canvas, 25267 alphaMode: "premultiply-alpha-on-upload", 25268 resolution, 25269 ...rest 25270 }); 25271 return createTexture(base, loader, url); 25272} 25273async function loadAsGraphics(url) { 25274 const response = await DOMAdapter.get().fetch(url); 25275 const svgSource = await response.text(); 25276 const context = new GraphicsContext(); 25277 context.svg(svgSource); 25278 return context; 25279} 25280 25281const WORKER_CODE$1 = "(function () {\n 'use strict';\n\n const WHITE_PNG = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=\";\n async function checkImageBitmap() {\n try {\n if (typeof createImageBitmap !== \"function\")\n return false;\n const response = await fetch(WHITE_PNG);\n const imageBlob = await response.blob();\n const imageBitmap = await createImageBitmap(imageBlob);\n return imageBitmap.width === 1 && imageBitmap.height === 1;\n } catch (_e) {\n return false;\n }\n }\n void checkImageBitmap().then((result) => {\n self.postMessage(result);\n });\n\n})();\n"; 25282let WORKER_URL$1 = null; 25283let WorkerInstance$1 = class WorkerInstance 25284{ 25285 constructor() 25286 { 25287 if (!WORKER_URL$1) 25288 { 25289 WORKER_URL$1 = URL.createObjectURL(new Blob([WORKER_CODE$1], { type: 'application/javascript' })); 25290 } 25291 this.worker = new Worker(WORKER_URL$1); 25292 } 25293}; 25294WorkerInstance$1.revokeObjectURL = function revokeObjectURL() 25295{ 25296 if (WORKER_URL$1) 25297 { 25298 URL.revokeObjectURL(WORKER_URL$1); 25299 WORKER_URL$1 = null; 25300 } 25301}; 25302 25303const WORKER_CODE = "(function () {\n 'use strict';\n\n async function loadImageBitmap(url, alphaMode) {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`[WorkerManager.loadImageBitmap] Failed to fetch ${url}: ${response.status} ${response.statusText}`);\n }\n const imageBlob = await response.blob();\n return alphaMode === \"premultiplied-alpha\" ? createImageBitmap(imageBlob, { premultiplyAlpha: \"none\" }) : createImageBitmap(imageBlob);\n }\n self.onmessage = async (event) =>
25303 {\n try {\n const imageBitmap = await loadImageBitmap(event.data.data[0], event.data.data[1]);\n self.postMessage({\n data: imageBitmap,\n uuid: event.data.uuid,\n id: event.data.id\n }, [imageBitmap]);\n } catch (e) {\n self.postMessage({\n error: e,\n uuid: event.data.uuid,\n id: event.data.id\n });\n }\n };\n\n})();\n"; 25304let WORKER_URL = null; 25305class WorkerInstance 25306{ 25307 constructor() 25308 { 25309 if (!WORKER_URL) 25310 { 25311 WORKER_URL = URL.createObjectURL(new Blob([WORKER_CODE], { type: 'application/javascript' })); 25312 } 25313 this.worker = new Worker(WORKER_URL); 25314 } 25315} 25316WorkerInstance.revokeObjectURL = function revokeObjectURL() 25317{ 25318 if (WORKER_URL) 25319 { 25320 URL.revokeObjectURL(WORKER_URL); 25321 WORKER_URL = null; 25322 } 25323}; 25324 25325let UUID = 0; 25326let MAX_WORKERS; 25327class WorkerManagerClass { 25328 constructor() { 25329 /** Whether the worker manager has been initialized */ 25330 this._initialized = false; 25331 /** Current number of created workers (used to enforce MAX_WORKERS limit) */ 25332 this._createdWorkers = 0; 25333 this._workerPool = []; 25334 this._queue = []; 25335 this._resolveHash = {}; 25336 } 25337 /** 25338 * Checks if ImageBitmap is supported in the current environment. 25339 * 25340 * This method uses a dedicated worker to test ImageBitmap support 25341 * and caches the result for subsequent calls. 25342 * @returns Promise that resolves to true if ImageBitmap is supported, false otherwise 25343 */ 25344 isImageBitmapSupported() { 25345 if (this._isImageBitmapSupported !== void 0) 25346 return this._isImageBitmapSupported; 25347 this._isImageBitmapSupported = new Promise((resolve) => { 25348 const { worker } = new WorkerInstance$1(); 25349 worker.addEventListener("message", (event) => { 25350 worker.terminate(); 25351 WorkerInstance$1.revokeObjectURL(); 25352 resolve(event.data); 25353 }); 25354 }); 25355 return this._isImageBitmapSupported; 25356 } 25357 /** 25358 * Loads an image as an ImageBitmap using a web worker. 25359 * @param src - The source URL or path of the image to load 25360 * @param asset - Optional resolved asset containing additional texture source options 25361 * @returns Promise that resolves to the loaded ImageBitmap 25362 * @example 25363 * ```typescript 25364 * const bitmap = await WorkerManager.loadImageBitmap('image.png'); 25365 * const bitmapWithOptions = await WorkerManager.loadImageBitmap('image.png', asset); 25366 * ``` 25367 */ 25368 loadImageBitmap(src, asset) { 25369 return this._run("loadImageBitmap", [src, asset?.data?.alphaMode]); 25370 } 25371 /** 25372 * Initializes the worker pool if not already initialized. 25373 * Currently a no-op but reserved for future initialization logic. 25374 */ 25375 async _initWorkers() { 25376 if (this._initialized) 25377 return; 25378 this._initialized = true; 25379 } 25380 /** 25381 * Gets an available worker from the pool or creates a new one if needed. 25382 * 25383 * Workers are created up to the MAX_WORKERS limit (based on navigator.hardwareConcurrency). 25384 * Each worker is configured with a message handler for processing results. 25385 * @returns Available worker or undefined if pool is at capacity and no workers are free 25386 */ 25387 _getWorker() { 25388 if (MAX_WORKERS === void 0) { 25389 MAX_WORKERS = navigator.hardwareConcurrency || 4; 25390 } 25391 let worker = this._workerPool.pop(); 25392 if (!worker && this._createdWorkers < MAX_WORKERS) { 25393 this._createdWorkers++; 25394 worker = new WorkerInstance().worker; 25395 worker.addEventListener("message", (event) => { 25396 this._complete(event.data); 25397 this._returnWorker(event.target); 25398 this._next(); 25399 }); 25400 } 25401 return worker; 25402 } 25403 /** 25404 * Returns a worker to the pool after completing a task. 25405 * @param worker - The worker to return to the pool 25406 */ 25407 _returnWorker(worker) { 25408 this._workerPool.push(worker); 25409 } 25410 /** 25411 * Handles completion of a worker task by resolving or rejecting the corresponding promise. 25412 * @param data - Result data from the worker containing uuid, data, and optional error 25413 */ 25414 _complete(data) { 25415 if (!this._resolveHash[data.uuid]) { 25416 return; 25417 } 25418 if (data.error !== void 0) { 25419 this._resolveHash[data.uuid].reject(data.error); 25420 } else { 25421 this._resolveHash[data.uuid].resolve(data.data); 25422 } 25423 delete this._resolveHash[data.uuid]; 25424 } 25425 /** 25426 * Executes a task using the worker pool system. 25427 *
25428 * Queues the task and processes it when a worker becomes available. 25429 * @param id - Identifier for the type of task to run 25430 * @param args - Arguments to pass to the worker 25431 * @returns Promise that resolves with the worker's result 25432 */ 25433 async _run(id, args) { 25434 await this._initWorkers(); 25435 const promise = new Promise((resolve, reject) => { 25436 this._queue.push({ id, arguments: args, resolve, reject }); 25437 }); 25438 this._next(); 25439 return promise; 25440 } 25441 /** 25442 * Processes the next item in the queue if workers are available. 25443 * 25444 * This method is called after worker initialization and when workers 25445 * complete tasks to continue processing the queue. 25446 */ 25447 _next() { 25448 if (!this._queue.length) 25449 return; 25450 const worker = this._getWorker(); 25451 if (!worker) { 25452 return; 25453 } 25454 const toDo = this._queue.pop(); 25455 const id = toDo.id; 25456 this._resolveHash[UUID] = { resolve: toDo.resolve, reject: toDo.reject }; 25457 worker.postMessage({ 25458 data: toDo.arguments, 25459 uuid: UUID++, 25460 id 25461 }); 25462 } 25463 /** 25464 * Resets the worker manager, terminating all workers and clearing the queue. 25465 * 25466 * This method: 25467 * - Terminates all active workers 25468 * - Rejects all pending promises with an error 25469 * - Clears all internal state 25470 * - Resets initialization flags 25471 * 25472 * This should be called when the worker manager is no longer needed 25473 * to prevent memory leaks and ensure proper cleanup. 25474 * @example 25475 * ```typescript 25476 * // Clean up when shutting down 25477 * WorkerManager.reset(); 25478 * ``` 25479 */ 25480 reset() { 25481 this._workerPool.forEach((worker) => worker.terminate()); 25482 this._workerPool.length = 0; 25483 Object.values(this._resolveHash).forEach(({ reject }) => { 25484 reject?.(new Error("WorkerManager has been reset before completion")); 25485 }); 25486 this._resolveHash = {}; 25487 this._queue.length = 0; 25488 this._initialized = false; 25489 this._createdWorkers = 0; 25490 } 25491} 25492const WorkerManager = new WorkerManagerClass(); 25493 25494const validImageExtensions = [".jpeg", ".jpg", ".png", ".webp", ".avif"]; 25495const validImageMIMEs = [ 25496 "image/jpeg", 25497 "image/png", 25498 "image/webp", 25499 "image/avif" 25500]; 25501async function loadImageBitmap(url, asset) { 25502 const response = await DOMAdapter.get().fetch(url); 25503 if (!response.ok) { 25504 throw new Error(`[loadImageBitmap] Failed to fetch ${url}: ${response.status} ${response.statusText}`); 25505 } 25506 const imageBlob = await response.blob(); 25507 return asset?.data?.alphaMode === "premultiplied-alpha" ? createImageBitmap(imageBlob, { premultiplyAlpha: "none" }) : createImageBitmap(imageBlob); 25508} 25509const loadTextures = { 25510 /** used for deprecation purposes */ 25511 name: "loadTextures", 25512 id: "texture", 25513 extension: { 25514 type: ExtensionType.LoadParser, 25515 priority: LoaderParserPriority.High, 25516 name: "loadTextures" 25517 }, 25518 config: { 25519 preferWorkers: true, 25520 preferCreateImageBitmap: true, 25521 crossOrigin: "anonymous" 25522 }, 25523 test(url) { 25524 return checkDataUrl(url, validImageMIMEs) || checkExtension(url, validImageExtensions); 25525 }, 25526 async load(url, asset, loader) { 25527 let src = null; 25528 if (globalThis.createImageBitmap && this.config.preferCreateImageBitmap) { 25529 if (this.config.preferWorkers && await WorkerManager.isImageBitmapSupported()) { 25530 src = await WorkerManager.loadImageBitmap(url, asset); 25531 } else { 25532 src = await loadImageBitmap(url, asset); 25533 } 25534 } else { 25535 src = await new Promise((resolve, reject) => { 25536 src = DOMAdapter.get().createImage(); 25537 src.crossOrigin = this.config.crossOrigin; 25538 src.src = url; 25539 if (src.complete) { 25540 resolve(src); 25541 } else { 25542 src.onload = () => { 25543 resolve(src); 25544 }; 25545 src.onerror = reject; 25546 } 25547 }); 25548 } 25549 const base = new ImageSource({ 25550 resource: src, 25551 alphaMode: "premultiply-alpha-on-upload", 25552 resolution: asset.data?.resolution || getResolutionOfUrl(url), 25553 ...asset.data 25554 }); 25555 return createTexture(base, loader, url); 25556 }, 25557 unload(texture) { 25558 texture.destroy(true); 25559 } 25560}; 25561 25562const potentialVideoExtensions = [".mp4", ".m4v", ".webm", ".ogg", ".ogv", ".h264", ".avi", ".mov"]; 25563let validVideoExtensions; 25564let validVideoMIMEs; 25565function crossOrigin(element, url, crossorigin) { 25566 if (crossorigin === void 0 && !url.startsWith("data:")) { 25567 element.crossOrigin = determineCrossOrigin(url); 25568 } else if (crossorigin !== false) { 25569 element.crossOrigin = typeof crossorigin === "string" ? crossorigin : "anonymous"; 25570 } 25571} 25572function preloadVideo(element) { 25573 return new Promise((resolve, reject) => { 25574 element.addEventListener("canplaythrough", loaded); 25575 element.addEventListener("error", error); 25576 element.load(); 25577 function loaded() { 25578 cleanup(); 25579 resolve(); 25580 } 25581 function error(err) { 25582 cleanup(); 25583 reject(err); 25584 } 25585 function cleanup() { 25586 element.removeEventListener("canplaythrough", loaded); 25587 element.removeEventListener("error", error); 25588 } 25589 }); 25590} 25591function determineCrossOrigin(url, loc = globalThis.location) { 25592 if (url.startsWith("data:")) { 25593 return ""; 25594 } 25595 loc || (loc = globalThis.location); 25596 const parsedUrl = new URL(url, document.baseURI); 25597 if (parsedUrl.hostname !== loc.hostname || parsedUrl.port !== loc.port || parsedUrl.protocol !== loc.protocol) { 25598 return "anonymous"; 25599 } 25600 return ""; 25601} 25602function getBrowserSupportedVideoExtensions() { 25603 const supportedExtensions = []; 25604 const supportedMimes = []; 25605 for (const ext of potentialVideoExtensions) { 25606 const mimeType = VideoSource.MIME_TYPES[ext.substring(1)] || `video/${ext.substring(1)}`; 25607 if (testVideoFormat(mimeType)) { 25608 supportedExtensions.push(ext); 25609 if (!supportedMimes.includes(mimeType)) { 25610 supportedMimes.push(mimeType); 25611 } 25612 } 25613 } 25614 return { 25615 validVideoExtensions: supportedExtensions, 25616 validVideoMime: supportedMimes 25617 }; 25618} 25619const loadVideoTextures = { 25620 /** used for deprecation purposes */ 25621 name: "loadVideo", 25622 id: "video", 25623 extension: { 25624 type: ExtensionType.LoadParser, 25625 name: "loadVideo" 25626 }, 25627 test(url) { 25628 if (!validVideoExtensions || !validVideoMIMEs) { 25629 const { validVideoExtensions: ve, validVideoMime: vm } = getBrowserSupportedVideoExtensions(); 25630 validVideoExtensions = ve; 25631 validVideoMIMEs = vm; 25632 } 25633 const isValidDataUrl = checkDataUrl(url, validVideoMIMEs); 25634 const isValidExtension = checkExtension(url, validVideoExtensions); 25635 return isValidDataUrl || isValidExtension; 25636 }, 25637 async load(url, asset, loader) { 25638 const options = { 25639 ...VideoSource.defaultOptions, 25640 resolution: asset.data?.resolution || getResolutionOfUrl(url), 25641 alphaMode: asset.data?.alphaMode || await detectVideoAlphaMode(), 25642 ...asset.data 25643 }; 25644 const videoElement = document.createElement("video"); 25645 const attributeMap = { 25646 preload: options.autoLoad !== false ? "auto" : void 0, 25647 "webkit-playsinline": options.playsinline !== false ? "" : void 0, 25648 playsinline: options.playsinline !== false ? "" : void 0, 25649 muted: options.muted === true ? "" : void 0, 25650 loop: options.loop === true ? "" : void 0, 25651 autoplay: options.autoPlay !== false ? "" : void 0 25652 };
25653 Object.keys(attributeMap).forEach((key) => { 25654 const value = attributeMap[key]; 25655 if (value !== void 0) 25656 videoElement.setAttribute(key, value); 25657 }); 25658 if (options.muted === true) { 25659 videoElement.muted = true; 25660 } 25661 crossOrigin(videoElement, url, options.crossorigin); 25662 const sourceElement = document.createElement("source"); 25663 let mime; 25664 if (options.mime) { 25665 mime = options.mime; 25666 } else if (url.startsWith("data:")) { 25667 mime = url.slice(5, url.indexOf(";")); 25668 } else if (!url.startsWith("blob:")) { 25669 const ext = url.split("?")[0].slice(url.lastIndexOf(".") + 1).toLowerCase(); 25670 mime = VideoSource.MIME_TYPES[ext] || `video/${ext}`; 25671 } 25672 sourceElement.src = url; 25673 if (mime) { 25674 sourceElement.type = mime; 25675 } 25676 return new Promise((resolve) => { 25677 const onCanPlay = async () => { 25678 const base = new VideoSource({ ...options, resource: videoElement }); 25679 videoElement.removeEventListener("canplay", onCanPlay); 25680 if (asset.data.preload) { 25681 await preloadVideo(videoElement); 25682 } 25683 resolve(createTexture(base, loader, url)); 25684 }; 25685 if (options.preload && !options.autoPlay) { 25686 videoElement.load(); 25687 } 25688 videoElement.addEventListener("canplay", onCanPlay); 25689 videoElement.appendChild(sourceElement); 25690 }); 25691 }, 25692 unload(texture) { 25693 texture.destroy(true); 25694 } 25695}; 25696 25697const resolveTextureUrl = { 25698 extension: { 25699 type: ExtensionType.ResolveParser, 25700 name: "resolveTexture" 25701 }, 25702 test: loadTextures.test, 25703 parse: (value) => ({ 25704 resolution: parseFloat(Resolver.RETINA_PREFIX.exec(value)?.[1] ?? "1"), 25705 format: value.split(".").pop(), 25706 src: value 25707 }) 25708}; 25709 25710const resolveJsonUrl = { 25711 extension: { 25712 type: ExtensionType.ResolveParser, 25713 priority: -2, 25714 name: "resolveJson" 25715 }, 25716 test: (value) => Resolver.RETINA_PREFIX.test(value) && value.endsWith(".json"), 25717 parse: resolveTextureUrl.parse 25718}; 25719 25720class AssetsClass { 25721 constructor() { 25722 this._detections = []; 25723 this._initialized = false; 25724 this.resolver = new Resolver(); 25725 this.loader = new Loader(); 25726 this.cache = Cache; 25727 this._backgroundLoader = new BackgroundLoader(this.loader); 25728 this._backgroundLoader.active = true; 25729 this.reset(); 25730 } 25731 /** 25732 * Initializes the Assets class with configuration options. While not required, 25733 * calling this before loading assets is recommended to set up default behaviors. 25734 * @param options - Configuration options for the Assets system 25735 * @example 25736 * ```ts 25737 * // Basic initialization (optional as Assets.load will call this automatically) 25738 * await Assets.init(); 25739 * 25740 * // With CDN configuration 25741 * await Assets.init({ 25742 * basePath: 'https://my-cdn.com/assets/', 25743 * defaultSearchParams: { version: '1.0.0' } 25744 * }); 25745 * 25746 * // With manifest and preferences 25747 * await Assets.init({ 25748 * manifest: { 25749 * bundles: [{ 25750 * name: 'game-screen', 25751 * assets: [ 25752 * { 25753 * alias: 'hero', 25754 * src: 'hero.{png,webp}', 25755 * data: { scaleMode: SCALE_MODES.NEAREST } 25756 * }, 25757 * { 25758 * alias: 'map', 25759 * src: 'map.json' 25760 * } 25761 * ] 25762 * }] 25763 * }, 25764 * // Optimize for device capabilities 25765 * texturePreference: { 25766 * resolution: window.devicePixelRatio, 25767 * format: ['webp', 'png'] 25768 * }, 25769 * // Set global preferences 25770 * preferences: { 25771 * crossOrigin: 'anonymous', 25772 * } 25773 * }); 25774 * 25775 * // Load assets after initialization 25776 * const heroTexture = await Assets.load('hero'); 25777 * ``` 25778 * @remarks 25779 * - Can be called only once; subsequent calls will be ignored with a warning 25780 * - Format detection runs automatically unless `skipDetections` is true 25781 * - The manifest can be a URL to a JSON file or an inline object 25782 * @see {@link AssetInitOptions} For all available initialization options 25783 * @see {@link AssetsManifest} For manifest format details 25784 */
25785 async init(options = {}) { 25786 if (this._initialized) { 25787 warn("[Assets]AssetManager already initialized, did you load before calling this Assets.init()?"); 25788 return; 25789 } 25790 this._initialized = true; 25791 if (options.defaultSearchParams) { 25792 this.resolver.setDefaultSearchParams(options.defaultSearchParams); 25793 } 25794 if (options.basePath) { 25795 this.resolver.basePath = options.basePath; 25796 } 25797 if (options.bundleIdentifier) { 25798 this.resolver.setBundleIdentifier(options.bundleIdentifier); 25799 } 25800 if (options.manifest) { 25801 let manifest = options.manifest; 25802 if (typeof manifest === "string") { 25803 manifest = await this.load(manifest); 25804 } 25805 this.resolver.addManifest(manifest); 25806 } 25807 const resolutionPref = options.texturePreference?.resolution ?? 1; 25808 const resolution = typeof resolutionPref === "number" ? [resolutionPref] : resolutionPref; 25809 const formats = await this._detectFormats({ 25810 preferredFormats: options.texturePreference?.format, 25811 skipDetections: options.skipDetections, 25812 detections: this._detections 25813 }); 25814 this.resolver.prefer({ 25815 params: { 25816 format: formats, 25817 resolution 25818 } 25819 }); 25820 if (options.preferences) { 25821 this.setPreferences(options.preferences); 25822 } 25823 if (options.loadOptions) { 25824 this.loader.loadOptions = { 25825 ...this.loader.loadOptions, 25826 ...options.loadOptions 25827 }; 25828 } 25829 } 25830 /** 25831 * Registers assets with the Assets resolver. This method maps keys (aliases) to asset sources, 25832 * allowing you to load assets using friendly names instead of direct URLs. 25833 * @param assets - The unresolved assets to add to the resolver 25834 * @example 25835 * ```ts 25836 * // Basic usage - single asset 25837 * Assets.add({ 25838 * alias: 'myTexture', 25839 * src: 'assets/texture.png' 25840 * }); 25841 * const texture = await Assets.load('myTexture'); 25842 * 25843 * // Multiple aliases for the same asset 25844 * Assets.add({ 25845 * alias: ['hero', 'player'], 25846 * src: 'hero.png' 25847 * }); 25848 * const hero1 = await Assets.load('hero'); 25849 * const hero2 = await Assets.load('player'); // Same texture 25850 * 25851 * // Multiple format support 25852 * Assets.add({ 25853 * alias: 'character', 25854 * src: 'character.{webp,png}' // Will choose best format 25855 * }); 25856 * Assets.add({ 25857 * alias: 'character', 25858 * src: ['character.webp', 'character.png'], // Explicitly specify formats 25859 * }); 25860 * 25861 * // With texture options 25862 * Assets.add({ 25863 * alias: 'sprite', 25864 * src: 'sprite.png', 25865 * data: { scaleMode: 'nearest' } 25866 * }); 25867 * 25868 * // Multiple assets at once 25869 * Assets.add([ 25870 * { alias: 'bg', src: 'background.png' }, 25871 * { alias: 'music', src: 'music.mp3' }, 25872 * { alias: 'spritesheet', src: 'sheet.json', data: { ignoreMultiPack: false } } 25873 * ]); 25874 * ``` 25875 * @remarks 25876 * - Assets are resolved when loaded, not when added 25877 * - Multiple formats use the best available format for the browser 25878 * - Adding with same alias overwrites previous definition 25879 * - The `data` property is passed to the asset loader 25880 * @see {@link Resolver} For details on asset resolution 25881 * @see {@link LoaderParser} For asset-specific data options 25882 * @advanced 25883 */ 25884 add(assets) { 25885 this.resolver.add(assets); 25886 } 25887 async load(urls, onProgress) { 25888 if (!this._initialized) { 25889 await this.init(); 25890 } 25891 const singleAsset = isSingleItem(urls); 25892 const urlArray = convertToList(urls).map((url) => { 25893 if (typeof url !== "string") { 25894 const aliases = this.resolver.getAlias(url); 25895 if (aliases.some((alias) => !this.resolver.hasKey(alias))) { 25896 this.add(url); 25897 } 25898 return Array.isArray(aliases) ? aliases[0] : aliases; 25899 } 25900 if (!this.resolver.hasKey(url)) 25901 this.add({ alias: url, src: url }); 25902 return url; 25903 }); 25904 const resolveResults = this.resolver.resolve(urlArray); 25905 const out = await this._mapLoadToResolve(resolveResults, onProgress); 25906 return singleAsset ? out[urlArray[0]] : out; 25907 } 25908 /** 25909 * Registers a bundle of assets that can be loaded as a group. Bundles are useful for organizing 25910 * assets into logical groups, such as game levels or UI screens. 25911 * @param bundleId - Unique identifier for the bundle 25912 * @param assets - Assets to include in the bundle 25913 * @example 25914 * ```ts 25915 * // Add a bundle using array format 25916 * Assets.addBundle('animals', [ 25917 * { alias: 'bunny', src: 'bunny.png' }, 25918 * { alias: 'chicken', src: 'chicken.png' }, 25919 * { alias: 'thumper', src: 'thumper.png' }, 25920 * ]); 25921 * 25922 * // Add a bundle using object format 25923 * Assets.addBundle('animals', { 25924 * bunny: 'bunny.png', 25925 * chicken: 'chicken.png', 25926 * thumper: 'thumper.png', 25927 * }); 25928 * 25929 * // Add a bundle with advanced options 25930 * Assets.addBundle('ui', [ 25931 * { 25932 * alias: 'button', 25933 * src: 'button.{webp,png}', 25934 * data: { scaleMode: 'nearest' } 25935 * }, 25936 * { 25937 * alias: ['logo', 'brand'], // Multiple aliases 25938 * src: 'logo.svg', 25939 * data: { resolution: 2 } 25940 * } 25941 * ]); 25942 * 25943 * // Load the bundle 25944 * await Assets.loadBundle('animals'); 25945 * 25946 * // Use the loaded assets 25947 * const bunny = Sprite.from('bunny'); 25948 * const chicken = Sprite.from('chicken'); 25949 * ``` 25950 * @remarks
25951 * - Bundle IDs must be unique 25952 * - Assets in bundles are not loaded until `loadBundle` is called 25953 * - Bundles can be background loaded using `backgroundLoadBundle` 25954 * - Assets in bundles can be loaded individually using their aliases 25955 * @see {@link Assets.loadBundle} For loading bundles 25956 * @see {@link Assets.backgroundLoadBundle} For background loading bundles 25957 * @see {@link Assets.unloadBundle} For unloading bundles 25958 * @see {@link AssetsManifest} For manifest format details 25959 */ 25960 addBundle(bundleId, assets) { 25961 this.resolver.addBundle(bundleId, assets); 25962 } 25963 /** 25964 * Loads a bundle or multiple bundles of assets. Bundles are collections of related assets 25965 * that can be loaded together. 25966 * @param bundleIds - Single bundle ID or array of bundle IDs to load 25967 * @param onProgress - Optional callback for load progress (0.0 to 1.0) 25968 * @returns Promise that resolves with the loaded bundle assets 25969 * @example 25970 * ```ts 25971 * // Define bundles in your manifest 25972 * const manifest = { 25973 * bundles: [ 25974 * { 25975 * name: 'load-screen', 25976 * assets: [ 25977 * { 25978 * alias: 'background', 25979 * src: 'sunset.png', 25980 * }, 25981 * { 25982 * alias: 'bar', 25983 * src: 'load-bar.{png,webp}', // use an array of individual assets 25984 * }, 25985 * ], 25986 * }, 25987 * { 25988 * name: 'game-screen', 25989 * assets: [ 25990 * { 25991 * alias: 'character', 25992 * src: 'robot.png', 25993 * }, 25994 * { 25995 * alias: 'enemy', 25996 * src: 'bad-guy.png', 25997 * }, 25998 * ], 25999 * }, 26000 * ] 26001 * }; 26002 * 26003 * // Initialize with manifest 26004 * await Assets.init({ manifest }); 26005 * 26006 * // Or add bundles programmatically 26007 * Assets.addBundle('load-screen', [...]); 26008 * Assets.loadBundle('load-screen'); 26009 * 26010 * // Load a single bundle 26011 * await Assets.loadBundle('load-screen'); 26012 * const bg = Sprite.from('background'); // Uses alias from bundle 26013 * 26014 * // Load multiple bundles 26015 * await Assets.loadBundle([ 26016 * 'load-screen', 26017 * 'game-screen' 26018 * ]); 26019 * 26020 * // Load with progress tracking 26021 * await Assets.loadBundle('game-screen', (progress) => { 26022 * console.log(`Loading: ${Math.round(progress * 100)}%`); 26023 * }); 26024 * ``` 26025 * @remarks 26026 * - Bundle assets are cached automatically 26027 * - Bundles can be pre-loaded using `backgroundLoadBundle` 26028 * - Assets in bundles can be accessed by their aliases 26029 * - Progress callback receives values from 0.0 to 1.0 26030 * @throws {Error} If the bundle ID doesn't exist in the manifest 26031 * @see {@link Assets.addBundle} For adding bundles programmatically 26032 * @see {@link Assets.backgroundLoadBundle} For background loading bundles 26033 * @see {@link Assets.unloadBundle} For unloading bundles 26034 * @see {@link AssetsManifest} For manifest format details 26035 */ 26036 async loadBundle(bundleIds, onProgress) { 26037 if (!this._initialized) { 26038 await this.init(); 26039 } 26040 let singleAsset = false; 26041 if (typeof bundleIds === "string") { 26042 singleAsset = true; 26043 bundleIds = [bundleIds]; 26044 } 26045 const resolveResults = this.resolver.resolveBundle(bundleIds); 26046 const out = {}; 26047 const keys = Object.keys(resolveResults); 26048 let total = 0; 26049 const counts = []; 26050 const _onProgress = () => { 26051 onProgress?.
vendor: 11,632 bytes, lines 26051-26383
26051(counts.reduce((a, b) => a + b, 0) / total); 26052 }; 26053 const promises = keys.map((bundleId, i) => { 26054 const resolveResult = resolveResults[bundleId]; 26055 const values = Object.values(resolveResult); 26056 const totalAssetsToLoad = [...new Set(values.flat())]; 26057 const progressSize = totalAssetsToLoad.reduce((sum, asset) => sum + (asset.progressSize || 1), 0); 26058 counts.push(0); 26059 total += progressSize; 26060 return this._mapLoadToResolve(resolveResult, (e) => { 26061 counts[i] = e * progressSize; 26062 _onProgress(); 26063 }).then((resolveResult2) => { 26064 out[bundleId] = resolveResult2; 26065 }); 26066 }); 26067 await Promise.all(promises); 26068 return singleAsset ? out[bundleIds[0]] : out; 26069 } 26070 /** 26071 * Initiates background loading of assets. This allows assets to be loaded passively while other operations 26072 * continue, making them instantly available when needed later. 26073 * 26074 * Background loading is useful for: 26075 * - Preloading game levels while in a menu 26076 * - Loading non-critical assets during gameplay 26077 * - Reducing visible loading screens 26078 * @param urls - Single URL/alias or array of URLs/aliases to load in the background 26079 * @example 26080 * ```ts 26081 * // Basic background loading 26082 * Assets.backgroundLoad('images/level2-assets.png'); 26083 * 26084 * // Background load multiple assets 26085 * Assets.backgroundLoad([ 26086 * 'images/sprite1.png', 26087 * 'images/sprite2.png', 26088 * 'images/background.png' 26089 * ]); 26090 * 26091 * // Later, when you need the assets 26092 * const textures = await Assets.load([ 26093 * 'images/sprite1.png', 26094 * 'images/sprite2.png' 26095 * ]); // Resolves immediately if background loading completed 26096 * ``` 26097 * @remarks 26098 * - Background loading happens one asset at a time to avoid blocking the main thread 26099 * - Loading can be interrupted safely by calling `Assets.load()` 26100 * - Assets are cached as they complete loading 26101 * - No progress tracking is available for background loading 26102 */ 26103 async backgroundLoad(urls) { 26104 if (!this._initialized) { 26105 await this.init(); 26106 } 26107 if (typeof urls === "string") { 26108 urls = [urls]; 26109 } 26110 const resolveResults = this.resolver.resolve(urls); 26111 this._backgroundLoader.add(Object.values(resolveResults)); 26112 } 26113 /** 26114 * Initiates background loading of asset bundles. Similar to backgroundLoad but works with 26115 * predefined bundles of assets. 26116 * 26117 * Perfect for: 26118 * - Preloading level bundles during gameplay 26119 * - Loading UI assets during splash screens 26120 * - Preparing assets for upcoming game states 26121 * @param bundleIds - Single bundle ID or array of bundle IDs to load in the background 26122 * @example 26123 * ```ts 26124 * // Define bundles in your manifest 26125 * await Assets.init({ 26126 * manifest: { 26127 * bundles: [ 26128 * { 26129 * name: 'home', 26130 * assets: [ 26131 * { 26132 * alias: 'background', 26133 * src: 'images/home-bg.png', 26134 * }, 26135 * { 26136 * alias: 'logo', 26137 * src: 'images/logo.png', 26138 * } 26139 * ] 26140 * }, 26141 * { 26142 * name: 'level-1', 26143 * assets: [ 26144 * { 26145 * alias: 'background', 26146 * src: 'images/level1/bg.png', 26147 * }, 26148 * { 26149 * alias: 'sprites', 26150 * src: 'images/level1/sprites.json' 26151 * } 26152 * ] 26153 * }] 26154 * } 26155 * }); 26156 * 26157 * // Load the home screen assets right away 26158 * await Assets.loadBundle('home'); 26159 * showHomeScreen(); 26160 * 26161 * // Start background loading while showing home screen 26162 * Assets.backgroundLoadBundle('level-1'); 26163 * 26164 * // When player starts level, load completes faster 26165 * await Assets.loadBundle('level-1'); 26166 * hideHomeScreen(); 26167 * startLevel(); 26168 * ``` 26169 * @remarks 26170 * - Bundle assets are loaded one at a time 26171 * - Loading can be interrupted safely by calling `Assets.loadBundle()` 26172 * - Assets are cached as they complete loading 26173 * - Requires bundles to be registered via manifest or `addBundle` 26174 * @see {@link Assets.addBundle} For adding bundles programmatically 26175 * @see {@link Assets.loadBundle} For immediate bundle loading 26176 * @see {@link AssetsManifest} For manifest format details 26177 */ 26178 async backgroundLoadBundle(bundleIds) { 26179 if (!this._initialized) { 26180 await this.init(); 26181 } 26182 if (typeof bundleIds === "string") { 26183 bundleIds = [bundleIds]; 26184 } 26185 const resolveResults = this.resolver.resolveBundle(bundleIds); 26186 Object.values(resolveResults).forEach((resolveResult) => { 26187 this._backgroundLoader.add(Object.values(resolveResult)); 26188 }); 26189 } 26190 /** 26191 * Only intended for development purposes. 26192 * This will wipe the resolver and caches. 26193 * You will need to reinitialize the Asset 26194 * @internal 26195 */ 26196 reset() { 26197 this.resolver.reset(); 26198 this.loader.reset(); 26199 this.cache.reset(); 26200 this._initialized = false; 26201 } 26202 get(keys) { 26203 if (typeof keys === "string") { 26204 return Cache.get(keys); 26205 } 26206 const assets = {}; 26207 for (let i = 0; i < keys.length; i++) { 26208 assets[i] = Cache.get(keys[i]); 26209 } 26210 return assets; 26211 } 26212 /** 26213 * helper function to map resolved assets back to loaded assets 26214 * @param resolveResults - the resolve results from the resolver 26215 * @param progressOrLoadOptions - the progress callback or load options 26216 */ 26217 async _mapLoadToResolve(resolveResults, progressOrLoadOptions) { 26218 const resolveArray = [...new Set(Object.values(resolveResults))]; 26219 this._backgroundLoader.active = false; 26220 const loadedAssets = await this.loader.load(resolveArray, progressOrLoadOptions); 26221 this._backgroundLoader.active = true; 26222 const out = {}; 26223 resolveArray.forEach((resolveResult) => { 26224 const asset = loadedAssets[resolveResult.src]; 26225 const keys = [resolveResult.src]; 26226 if (resolveResult.alias) { 26227 keys.push(...resolveResult.alias); 26228 } 26229 keys.forEach((key) => { 26230 out[key] = asset; 26231 }); 26232 Cache.set(keys, asset); 26233 }); 26234 return out; 26235 } 26236 /** 26237 * Unloads assets and releases them from memory. This method ensures proper cleanup of 26238 * loaded assets when they're no longer needed. 26239 * @param urls - Single URL/alias or array of URLs/aliases to unload 26240 * @example 26241 * ```ts 26242 * // Unload a single asset 26243 * await Assets.unload('images/sprite.png'); 26244 * 26245 * // Unload using an alias 26246 * await Assets.unload('hero'); // Unloads the asset registered with 'hero' alias 26247 * 26248 * // Unload multiple assets 26249 * await Assets.unload([ 26250 * 'images/background.png', 26251 * 'images/character.png', 26252 * 'hero' 26253 * ]); 26254 * 26255 * // Unload and handle creation of new instances 26256 * await Assets.unload('hero'); 26257 * const newHero = await Assets.load('hero'); // Will load fresh from source 26258 * ``` 26259 * @remarks 26260 * > [!WARNING] 26261 * > Make sure assets aren't being used before unloading: 26262 * > - Remove sprites using the texture 26263 * > - Clear any references to the asset 26264 * > - Textures will be destroyed and can't be used after unloading 26265 * @throws {Error} If the asset is not found in cache 26266 */ 26267 async unload(urls) { 26268 if (!this._initialized) { 26269 await this.init(); 26270 } 26271 const urlArray = convertToList(urls).map((url) => typeof url !== "string" ? url.src : url); 26272 const resolveResults = this.resolver.resolve(urlArray); 26273 await this._unloadFromResolved(resolveResults); 26274 } 26275 /** 26276 * Unloads all assets in a bundle. Use this to free memory when a bundle's assets 26277 * are no longer needed, such as when switching game levels. 26278 * @param bundleIds - Single bundle ID or array of bundle IDs to unload 26279 * @example 26280 * ```ts 26281 * // Define and load a bundle 26282 * Assets.addBundle('level-1', { 26283 * background: 'level1/bg.png', 26284 * sprites: 'level1/sprites.json', 26285 * music: 'level1/music.mp3' 26286 * }); 26287 * 26288 * // Load the bundle 26289 * const level1 = await Assets.loadBundle('level-1'); 26290 * 26291 * // Use the assets 26292 * const background = Sprite.from(level1.background); 26293 * 26294 * // When done with the level, unload everything 26295 * await Assets.unloadBundle('level-1'); 26296 * // background sprite is now invalid! 26297 * 26298 * // Unload multiple bundles 26299 * await Assets.unloadBundle([ 26300 * 'level-1', 26301 * 'level-2', 26302 * 'ui-elements' 26303 * ]); 26304 * ``` 26305 * @remarks 26306 * > [!WARNING] 26307 * > - All assets in the bundle will be destroyed 26308 * > - Bundle needs to be reloaded to use assets again 26309 * > - Make sure no sprites or other objects are using the assets 26310 * @throws {Error} If the bundle is not found 26311 * @see {@link Assets.addBundle} For adding bundles 26312 * @see {@link Assets.loadBundle} For loading bundles 26313 */ 26314 async unloadBundle(bundleIds) { 26315 if (!this._initialized) { 26316 await this.init(); 26317 } 26318 bundleIds = convertToList(bundleIds); 26319 const resolveResults = this.resolver.resolveBundle(bundleIds); 26320 const promises = Object.keys(resolveResults).map((bundleId) => this._unloadFromResolved(resolveResults[bundleId])); 26321 await Promise.all(promises); 26322 } 26323 async _unloadFromResolved(resolveResult) { 26324 const resolveArray = Object.values(resolveResult); 26325 resolveArray.forEach((resolveResult2) => { 26326 Cache.remove(resolveResult2.src); 26327 }); 26328 await this.loader.unload(resolveArray); 26329 } 26330 /** 26331 * Detects the supported formats for the browser, and returns an array of supported formats, respecting 26332 * the users preferred formats order. 26333 * @param options - the options to use when detecting formats 26334 * @param options.preferredFormats - the preferred formats to use 26335 * @param options.skipDetections - if we should skip the detections altogether 26336 * @param options.detections - the detections to use 26337 * @returns - the detected formats 26338 */ 26339 async _detectFormats(options) { 26340 let formats = []; 26341 if (options.preferredFormats) { 26342 formats = Array.isArray(options.preferredFormats) ? options.preferredFormats : [options.preferredFormats]; 26343 } 26344 for (const detection of options.detections) { 26345 if (options.skipDetections || await detection.test()) { 26346 formats = await detection.add(formats); 26347 } else if (!options.skipDetections) { 26348 formats = await detection.remove(formats); 26349 } 26350 } 26351 formats = formats.filter((format, index) => formats.indexOf(format) === index); 26352 return formats; 26353 } 26354 /** 26355 * All the detection parsers currently added to the Assets class. 26356 * @advanced 26357 */ 26358 get detections() { 26359 return this._detections; 26360 } 26361 /** 26362 * Sets global preferences for asset loading behavior. This method configures how assets 26363 * are loaded and processed across all parsers. 26364 * @param preferences - Asset loading preferences 26365 * @example 26366 * ```ts 26367 * // Basic preferences 26368 * Assets.setPreferences({ 26369 * crossOrigin: 'anonymous', 26370 * parseAsGraphicsContext: false 26371 * }); 26372 * ``` 26373 * @remarks 26374 * Preferences are applied to all compatible parsers and affect future asset loading. 26375 * Common preferences include: 26376 * - `crossOrigin`: CORS setting for loaded assets 26377 * - `preferWorkers`: Whether to use web workers for loading textures 26378 * - `preferCreateImageBitmap`: Use `createImageBitmap` for texture creation. Turning this off will use the `Image` constructor instead. 26379 * @see {@link AssetsPreferences} For all available preferences 26380 */ 26381 setPreferences(preferences) { 26382 this.loader.parsers.forEach((parser) => { 26383 if (!parser.config)
26383 26384 return; 26385 Object.keys(parser.config).filter((key) => key in preferences).forEach((key) => { 26386 parser.config[key] = preferences[key]; 26387 }); 26388 }); 26389 } 26390} 26391const Assets = new AssetsClass(); 26392extensions.handleByList(ExtensionType.LoadParser, Assets.loader.parsers).handleByList(ExtensionType.ResolveParser, Assets.resolver.parsers).handleByList(ExtensionType.CacheParser, Assets.cache.parsers).handleByList(ExtensionType.DetectionParser, Assets.detections); 26393extensions.add( 26394 cacheTextureArray, 26395 detectDefaults, 26396 detectAvif, 26397 detectWebp, 26398 detectMp4, 26399 detectOgv, 26400 detectWebm, 26401 loadJson, 26402 loadTxt, 26403 loadWebFont, 26404 loadSvg, 26405 loadTextures, 26406 loadVideoTextures, 26407 loadBitmapFont, 26408 bitmapFontCachePlugin, 26409 resolveTextureUrl, 26410 resolveJsonUrl 26411); 26412const assetKeyMap = { 26413 loader: ExtensionType.LoadParser, 26414 resolver: ExtensionType.ResolveParser, 26415 cache: ExtensionType.CacheParser, 26416 detection: ExtensionType.DetectionParser 26417}; 26418extensions.handle(ExtensionType.Asset, (extension) => { 26419 const ref = extension.ref; 26420 Object.entries(assetKeyMap).filter(([key]) => !!ref[key]).forEach(([key, type]) => extensions.add(Object.assign( 26421 ref[key], 26422 // Allow the function to optionally define it's own 26423 // ExtensionMetadata, the use cases here is priority for LoaderParsers 26424 { extension: ref[key].extension ?? type } 26425 ))); 26426}, (extension) => { 26427 const ref = extension.ref; 26428 Object.keys(assetKeyMap).filter((key) => !!ref[key]).forEach((key) => extensions.remove(ref[key])); 26429}); 26430 26431class Graphics extends ViewContainer { 26432 /** 26433 * Creates a new Graphics object. 26434 * @param options - Options for the Graphics. 26435 */ 26436 constructor(options) { 26437 if (options instanceof GraphicsContext) { 26438 options = { context: options }; 26439 } 26440 const { context, roundPixels, ...rest } = options || {}; 26441 super({ 26442 label: "Graphics", 26443 ...rest 26444 }); 26445 /** @internal */ 26446 this.renderPipeId = "graphics"; 26447 if (!context) { 26448 this.context = this._ownedContext = new GraphicsContext(); 26449 this.context.autoGarbageCollect = this.autoGarbageCollect; 26450 } else { 26451 this.context = context; 26452 } 26453 this.didViewUpdate = true; 26454 this.allowChildren = false; 26455 this.roundPixels = roundPixels ?? false; 26456 } 26457 set context(context) { 26458 if (context === this._context) 26459 return; 26460 if (this._context) { 26461 this._context.off("update", this.onViewUpdate, this); 26462 this._context.off("unload", this.unload, this); 26463 } 26464 this._context = context; 26465 this._context.on("update", this.onViewUpdate, this); 26466 this._context.on("unload", this.unload, this); 26467 this.onViewUpdate(); 26468 } 26469 /** 26470 * The underlying graphics context used for drawing operations. 26471 * Controls how shapes and paths are rendered. 26472 * @example 26473 * ```ts 26474 * // Create a shared context 26475 * const sharedContext = new GraphicsContext(); 26476 * 26477 * // Create graphics objects sharing the same context 26478 * const graphics1 = new Graphics(); 26479 * const graphics2 = new Graphics(); 26480 * 26481 * // Assign shared context 26482 * graphics1.context = sharedContext; 26483 * graphics2.context = sharedContext; 26484 * 26485 * // Both graphics will show the same shapes 26486 * sharedContext 26487 * .rect(0, 0, 100, 100) 26488 * .fill({ color: 0xff0000 }); 26489 * ``` 26490 * @see {@link GraphicsContext} For drawing operations 26491 * @see {@link GraphicsOptions} For context configuration 26492 */ 26493 get context() { 26494 return this._context; 26495 } 26496 /** 26497 * The local bounds of the graphics object. 26498 * Returns the boundaries after all graphical operations but before any transforms. 26499 * @example 26500 * ```ts 26501 * const graphics = new Graphics(); 26502 * 26503 * // Draw a shape 26504 * graphics 26505 * .rect(0, 0, 100, 100) 26506 * .fill({ color: 0xff0000 }); 26507 * 26508 * // Get bounds information 26509 * const bounds = graphics.bounds; 26510 * console.log(bounds.width); // 100 26511 * console.log(bounds.height); // 100 26512 * ``` 26513 * @readonly 26514 * @see {@link Bounds} For bounds operations 26515 * @see {@link Container#getBounds} For transformed bounds 26516 */ 26517 get bounds() { 26518 return this._context.bounds; 26519 } 26520 /** 26521 * Graphics objects do not need to update their bounds as the context handles this. 26522 * @private 26523 */ 26524 updateBounds() { 26525 } 26526 /**
26527 * Checks if the object contains the given point. 26528 * Returns true if the point lies within the Graphics object's rendered area. 26529 * @example 26530 * ```ts 26531 * const graphics = new Graphics(); 26532 * 26533 * // Draw a shape 26534 * graphics 26535 * .rect(0, 0, 100, 100) 26536 * .fill({ color: 0xff0000 }); 26537 * 26538 * // Check point intersection 26539 * if (graphics.containsPoint({ x: 50, y: 50 })) { 26540 * console.log('Point is inside rectangle!'); 26541 * } 26542 * ``` 26543 * @param point - The point to check in local coordinates 26544 * @returns True if the point is inside the Graphics object 26545 * @see {@link Graphics#bounds} For bounding box checks 26546 * @see {@link PointData} For point data structure 26547 */ 26548 containsPoint(point) { 26549 return this._context.containsPoint(point); 26550 } 26551 /** 26552 * Destroys this graphics renderable and optionally its context. 26553 * @param options - Options parameter. A boolean will act as if all options 26554 * 26555 * If the context was created by this graphics and `destroy(false)` or `destroy()` is called 26556 * then the context will still be destroyed. 26557 * 26558 * If you want to explicitly not destroy this context that this graphics created, 26559 * then you should pass destroy({ context: false }) 26560 * 26561 * If the context was passed in as an argument to the constructor then it will not be destroyed 26562 * @example 26563 * ```ts 26564 * // Destroy the graphics and its context 26565 * graphics.destroy(); 26566 * graphics.destroy(true); 26567 * graphics.destroy({ context: true, texture: true, textureSource: true }); 26568 * ``` 26569 */ 26570 destroy(options) { 26571 if (this._ownedContext && !options) { 26572 this._ownedContext.destroy(options); 26573 } else if (options === true || options?.context === true) { 26574 this._context.destroy(options); 26575 }
vendor: 18,393 bytes, lines 26575-27188
26575 26576 this._ownedContext = null; 26577 this._context = null; 26578 super.destroy(options); 26579 } 26580 /** 26581 * @param now - The current time in milliseconds. 26582 * @internal 26583 */ 26584 _onTouch(now) { 26585 this._gcLastUsed = now; 26586 this._context._gcLastUsed = now; 26587 } 26588 _callContextMethod(method, args) { 26589 this.context[method](...args); 26590 return this; 26591 } 26592 // --------------------------------------- GraphicsContext methods --------------------------------------- 26593 /** 26594 * Sets the current fill style of the graphics context. 26595 * The fill style can be a color, gradient, pattern, or a complex style object. 26596 * @example 26597 * ```ts 26598 * const graphics = new Graphics(); 26599 * 26600 * // Basic color fill 26601 * graphics 26602 * .setFillStyle({ color: 0xff0000 }) // Red fill 26603 * .rect(0, 0, 100, 100) 26604 * .fill(); 26605 * 26606 * // Gradient fill 26607 * const gradient = new FillGradient({ 26608 * end: { x: 1, y: 0 }, 26609 * colorStops: [ 26610 * { offset: 0, color: 0xff0000 }, // Red at start 26611 * { offset: 0.5, color: 0x00ff00 }, // Green at middle 26612 * { offset: 1, color: 0x0000ff }, // Blue at end 26613 * ], 26614 * }); 26615 * 26616 * graphics 26617 * .setFillStyle(gradient) 26618 * .circle(100, 100, 50) 26619 * .fill(); 26620 * 26621 * // Pattern fill 26622 * const pattern = new FillPattern(texture); 26623 * graphics 26624 * .setFillStyle({ 26625 * fill: pattern, 26626 * alpha: 0.5 26627 * }) 26628 * .rect(0, 0, 200, 200) 26629 * .fill(); 26630 * ``` 26631 * @param {FillInput} args - The fill style to apply 26632 * @returns The Graphics instance for chaining 26633 * @see {@link FillStyle} For fill style options 26634 * @see {@link FillGradient} For gradient fills 26635 * @see {@link FillPattern} For pattern fills 26636 */ 26637 setFillStyle(...args) { 26638 return this._callContextMethod("setFillStyle", args); 26639 } 26640 /** 26641 * Sets the current stroke style of the graphics context. 26642 * Similar to fill styles, stroke styles can encompass colors, gradients, patterns, or more detailed configurations. 26643 * @example 26644 * ```ts 26645 * const graphics = new Graphics(); 26646 * 26647 * // Basic color stroke 26648 * graphics 26649 * .setStrokeStyle({ 26650 * width: 2, 26651 * color: 0x000000 26652 * }) 26653 * .rect(0, 0, 100, 100) 26654 * .stroke(); 26655 * 26656 * // Complex stroke style 26657 * graphics 26658 * .setStrokeStyle({ 26659 * width: 4, 26660 * color: 0xff0000, 26661 * alpha: 0.5, 26662 * join: 'round', 26663 * cap: 'round', 26664 * alignment: 0.5 26665 * }) 26666 * .circle(100, 100, 50) 26667 * .stroke(); 26668 * 26669 * // Gradient stroke 26670 * const gradient = new FillGradient({ 26671 * end: { x: 1, y: 0 }, 26672 * colorStops: [ 26673 * { offset: 0, color: 0xff0000 }, // Red at start 26674 * { offset: 0.5, color: 0x00ff00 }, // Green at middle 26675 * { offset: 1, color: 0x0000ff }, // Blue at end 26676 * ], 26677 * }); 26678 * 26679 * graphics 26680 * .setStrokeStyle({ 26681 * width: 10, 26682 * fill: gradient 26683 * }) 26684 * .poly([0,0, 100,50, 0,100]) 26685 * .stroke(); 26686 * ``` 26687 * @param {StrokeInput} args - The stroke style to apply 26688 * @returns The Graphics instance for chaining 26689 * @see {@link StrokeStyle} For stroke style options 26690 * @see {@link FillGradient} For gradient strokes 26691 * @see {@link FillPattern} For pattern strokes 26692 */ 26693 setStrokeStyle(...args) { 26694 return this._callContextMethod("setStrokeStyle", args); 26695 } 26696 fill(...args) { 26697 return this._callContextMethod("fill", args); 26698 } 26699 /** 26700 * Strokes the current path with the current stroke style or specified style. 26701 * Outlines the shape using the stroke settings. 26702 * @example 26703 * ```ts 26704 * const graphics = new Graphics(); 26705 * 26706 * // Stroke with direct color 26707 * graphics 26708 * .circle(50, 50, 25) 26709 * .stroke({ 26710 * width: 2, 26711 * color: 0xff0000 26712 * }); // 2px red stroke 26713 * 26714 * // Fill with texture 26715 * graphics 26716 * .rect(0, 0, 100, 100) 26717 * .stroke(myTexture); // Fill with texture 26718 * 26719 * // Stroke with gradient 26720 * const gradient = new FillGradient({ 26721 * end: { x: 1, y: 0 }, 26722 * colorStops: [ 26723 * { offset: 0, color: 0xff0000 }, 26724 * { offset: 0.5, color: 0x00ff00 }, 26725 * { offset: 1, color: 0x0000ff }, 26726 * ], 26727 * }); 26728 * 26729 * graphics 26730 * .rect(0, 0, 100, 100) 26731 * .stroke({ 26732 * width: 4, 26733 * fill: gradient, 26734 * alignment: 0.5, 26735 * join: 'round' 26736 * }); 26737 * ``` 26738 * @param {StrokeStyle} args - Optional stroke style to apply. Can be: 26739 * - A stroke style object with width, color, etc. 26740 * - A gradient 26741 * - A pattern 26742 * If omitted, uses current stroke style. 26743 * @returns The Graphics instance for chaining 26744 * @see {@link StrokeStyle} For stroke style options 26745 * @see {@link FillGradient} For gradient strokes 26746 * @see {@link setStrokeStyle} For setting default stroke style 26747 */ 26748 stroke(...args) { 26749 return this._callContextMethod("stroke", args); 26750 } 26751 texture(...args) { 26752 return this._callContextMethod("texture", args); 26753 } 26754 /** 26755 * Resets the current path. Any previous path and its commands are discarded and a new path is 26756 * started. This is typically called before beginning a new shape or series of drawing commands. 26757 * @example 26758 * ```ts 26759 * const graphics = new Graphics(); 26760 * graphics 26761 * .circle(150, 150, 50) 26762 * .fill({ color: 0x00ff00 }) 26763 * .beginPath() // Starts a new path 26764 * .circle(250, 150, 50) 26765 * .fill({ color: 0x0000ff }); 26766 * ``` 26767 * @returns The Graphics instance for chaining 26768 * @see {@link Graphics#moveTo} For starting a new subpath 26769 * @see {@link Graphics#closePath} For closing the current path 26770 */ 26771 beginPath() { 26772 return this._callContextMethod("beginPath", []); 26773 } 26774 /** 26775 * Applies a cutout to the last drawn shape. This is used to create holes or complex shapes by 26776 * subtracting a path from the previously drawn path. 26777 * 26778 * If a hole is not completely in a shape, it will fail to cut correctly. 26779 * @example 26780 * ```ts 26781 * const graphics = new Graphics(); 26782 * 26783 * // Draw outer circle 26784 * graphics 26785 * .circle(100, 100, 50) 26786 * .fill({ color: 0xff0000 }); 26787 * .circle(100, 100, 25) // Inner circle 26788 * .cut() // Cuts out the inner circle from the outer circle 26789 * ``` 26790 */ 26791 cut() { 26792 return this._callContextMethod("cut", []); 26793 } 26794 arc(...args) { 26795 return this._callContextMethod("arc", args); 26796 } 26797 arcTo(...args) { 26798 return this._callContextMethod("arcTo", args); 26799 } 26800 arcToSvg(...args) { 26801 return this._callContextMethod("arcToSvg", args); 26802 } 26803 bezierCurveTo(...args) { 26804 return this._callContextMethod("bezierCurveTo", args); 26805 } 26806 /** 26807 * Closes the current path by drawing a straight line back to the start point. 26808 * 26809 * This is useful for completing shapes and ensuring they are properly closed for fills. 26810 * @example 26811 * ```ts 26812 * // Create a triangle with closed path 26813 * const graphics = new Graphics(); 26814 * graphics 26815 * .moveTo(50, 50) 26816 * .lineTo(100, 100) 26817 * .lineTo(0, 100) 26818 * .closePath() 26819 * ``` 26820 * @returns The Graphics instance for method chaining 26821 * @see {@link Graphics#beginPath} For starting a new path 26822 * @see {@link Graphics#fill} For filling closed paths 26823 * @see {@link Graphics#stroke} For stroking paths 26824 */ 26825 closePath() { 26826 return this._callContextMethod("closePath", []); 26827 } 26828 ellipse(...args) { 26829 return this._callContextMethod("ellipse", args); 26830 } 26831 circle(...args) { 26832 return this._callContextMethod("circle", args); 26833 } 26834 path(...args) { 26835 return this._callContextMethod("path", args); 26836 } 26837 lineTo(...args) { 26838 return this._callContextMethod("lineTo", args); 26839 } 26840 moveTo(...args) { 26841 return this._callContextMethod("moveTo", args); 26842 } 26843 quadraticCurveTo(...args) { 26844 return this._callContextMethod("quadraticCurveTo", args); 26845 } 26846 rect(...args) { 26847 return this._callContextMethod("rect", args); 26848 } 26849 roundRect(...args) { 26850 return this._callContextMethod("roundRect", args); 26851 } 26852 poly(...args) { 26853 return this._callContextMethod("poly", args); 26854 } 26855 regularPoly(...args) { 26856 return this._callContextMethod("regularPoly", args); 26857 } 26858 roundPoly(...args) { 26859 return this._callContextMethod("roundPoly", args); 26860 } 26861 roundShape(...args) { 26862 return this._callContextMethod("roundShape", args); 26863 } 26864 filletRect(...args) { 26865 return this._callContextMethod("filletRect", args); 26866 } 26867 chamferRect(...args) { 26868 return this._callContextMethod("chamferRect", args); 26869 } 26870 star(...args) { 26871 return this._callContextMethod("star", args); 26872 } 26873 svg(...args) { 26874 return this._callContextMethod("svg", args); 26875 } 26876 restore(...args) { 26877 return this._callContextMethod("restore", args); 26878 } 26879 /** 26880 * Saves the current graphics state onto a stack. The state includes: 26881 * - Current transformation matrix 26882 * - Current fill style 26883 * - Current stroke style 26884 * @example 26885 * ```ts 26886 * const graphics = new Graphics(); 26887 * 26888 * // Save state before complex operations 26889 * graphics.save(); 26890 * 26891 * // Create transformed and styled shape 26892 * graphics 26893 * .translateTransform(100, 100) 26894 * .rotateTransform(Math.PI / 4) 26895 * .setFillStyle({ 26896 * color: 0xff0000, 26897 * alpha: 0.5 26898 * }) 26899 * .rect(-25, -25, 50, 50) 26900 * .fill(); 26901 * 26902 * // Restore to original state 26903 * graphics.restore(); 26904 * 26905 * // Continue drawing with previous state 26906 * graphics 26907 * .circle(50, 50, 25) 26908 * .fill(); 26909 * ``` 26910 * @returns The Graphics instance for method chaining 26911 * @see {@link Graphics#restore} For restoring the saved state 26912 * @see {@link Graphics#setTransform} For setting transformations 26913 */ 26914 save() { 26915 return this._callContextMethod("save", []); 26916 } 26917 /** 26918 * Returns the current transformation matrix of the graphics context. 26919 * This matrix represents all accumulated transformations including translate, scale, and rotate. 26920 * @example 26921 * ```ts 26922 * const graphics = new Graphics(); 26923 * 26924 * // Apply some transformations 26925 * graphics 26926 * .translateTransform(100, 100) 26927 * .rotateTransform(Math.PI / 4); 26928 * 26929 * // Get the current transform matrix 26930 * const matrix = graphics.getTransform(); 26931 * console.log(matrix.tx, matrix.ty); // 100, 100 26932 * 26933 * // Use the matrix for other operations 26934 * graphics 26935 * .setTransform(matrix) 26936 * .circle(0, 0, 50) 26937 * .fill({ color: 0xff0000 }); 26938 * ``` 26939 * @returns The current transformation matrix. 26940 * @see {@link Graphics#setTransform} For setting the transform matrix 26941 * @see {@link Matrix} For matrix operations 26942 */ 26943 getTransform() { 26944 return this.context.getTransform(); 26945 } 26946 /** 26947 * Resets the current transformation matrix to the identity matrix, effectively removing 26948 * any transformations (rotation, scaling, translation) previously applied. 26949 * @example 26950 * ```ts 26951 * const graphics = new Graphics(); 26952 * 26953 * // Apply transformations 26954 * graphics 26955 * .translateTransform(100, 100) 26956 * .scaleTransform(2, 2) 26957 * .circle(0, 0, 25) 26958 * .fill({ color: 0xff0000 }); 26959 * // Reset transform to default state 26960 * graphics 26961 * .resetTransform() 26962 * .circle(50, 50, 25) // Will draw at actual coordinates 26963 * .fill({ color: 0x00ff00 }); 26964 * ``` 26965 * @returns The Graphics instance for method chaining 26966 * @see {@link Graphics#getTransform} For getting the current transform 26967 * @see {@link Graphics#setTransform} For setting a specific transform 26968 * @see {@link Graphics#save} For saving the current transform state 26969 * @see {@link Graphics#restore} For restoring a previous transform state 26970 */ 26971 resetTransform() { 26972 return this._callContextMethod("resetTransform", []); 26973 } 26974 rotateTransform(...args) { 26975 return this._callContextMethod("rotate", args); 26976 } 26977 scaleTransform(...args) { 26978 return this._callContextMethod("scale", args); 26979 } 26980 setTransform(...args) { 26981 return this._callContextMethod("setTransform", args); 26982 } 26983 transform(...args) { 26984 return this._callContextMethod("transform", args); 26985 } 26986 translateTransform(...args) { 26987 return this._callContextMethod("translate", args); 26988 } 26989 /** 26990 * Clears all drawing commands from the graphics context, effectively resetting it. 26991 * This includes clearing the current path, fill style, stroke style, and transformations. 26992 * 26993 * > [!NOTE] Graphics objects are not designed to be continuously cleared and redrawn. 26994 * > Instead, they are intended to be used for static or semi-static graphics that 26995 * > can be redrawn as needed. Frequent clearing and redrawing may lead to performance issues. 26996 * @example 26997 * ```ts 26998 * const graphics = new Graphics(); 26999 * 27000 * // Draw some shapes 27001 * graphics 27002 * .circle(100, 100, 50) 27003 * .fill({ color: 0xff0000 }) 27004 * .rect(200, 100, 100, 50) 27005 * .fill({ color: 0x00ff00 }); 27006 * 27007 * // Clear all graphics 27008 * graphics.clear(); 27009 * 27010 * // Start fresh with new shapes 27011 * graphics 27012 * .circle(150, 150, 30) 27013 * .fill({ color: 0x0000ff }); 27014 * ``` 27015 * @returns The Graphics instance for method chaining 27016 * @see {@link Graphics#beginPath} For starting a new path without clearing styles 27017 * @see {@link Graphics#save} For saving the current state 27018 * @see {@link Graphics#restore} For restoring a previous state 27019 */ 27020 clear() { 27021 return this._callContextMethod("clear", []); 27022 } 27023 /** 27024 * Gets or sets the current fill style for the graphics context. The fill style determines 27025 * how shapes are filled when using the fill() method. 27026 * @example 27027 * ```ts 27028 * const graphics = new Graphics(); 27029 * 27030 * // Basic color fill 27031 * graphics.fillStyle = { 27032 * color: 0xff0000, // Red 27033 * alpha: 1 27034 * }; 27035 * 27036 * // Using gradients 27037 * const gradient = new FillGradient({ 27038 * end: { x: 0, y: 1 }, // Vertical gradient 27039 * stops: [ 27040 * { offset: 0, color: 0xff0000, alpha: 1 }, // Start color 27041 * { offset: 1, color: 0x0000ff, alpha: 1 } // End color 27042 * ] 27043 * }); 27044 * 27045 * graphics.fillStyle = { 27046 * fill: gradient, 27047 * alpha: 0.8 27048 * }; 27049 * 27050 * // Using patterns 27051 * graphics.fillStyle = { 27052 * texture: myTexture, 27053 * alpha: 1, 27054 * matrix: new Matrix() 27055 * .scale(0.5, 0.5) 27056 * .rotate(Math.PI / 4) 27057 * }; 27058 * ``` 27059 * @type {ConvertedFillStyle} 27060 * @see {@link FillStyle} For all available fill style options 27061 * @see {@link FillGradient} For creating gradient fills 27062 * @see {@link Graphics#fill} For applying the fill to paths 27063 */ 27064 get fillStyle() { 27065 return this._context.fillStyle; 27066 } 27067 set fillStyle(value) { 27068 this._context.fillStyle = value; 27069 } 27070 /** 27071 * Gets or sets the current stroke style for the graphics context. The stroke style determines 27072 * how paths are outlined when using the stroke() method. 27073 * @example 27074 * ```ts 27075 * const graphics = new Graphics(); 27076 * 27077 * // Basic stroke style 27078 * graphics.strokeStyle = { 27079 * width: 2, 27080 * color: 0xff0000, 27081 * alpha: 1 27082 * }; 27083 * 27084 * // Using with gradients 27085 * const gradient = new FillGradient({ 27086 * end: { x: 0, y: 1 }, 27087 * stops: [ 27088 * { offset: 0, color: 0xff0000, alpha: 1 }, 27089 * { offset: 1, color: 0x0000ff, alpha: 1 } 27090 * ] 27091 * }); 27092 * 27093 * graphics.strokeStyle = { 27094 * width: 4, 27095 * fill: gradient, 27096 * alignment: 0.5, 27097 * join: 'round', 27098 * cap: 'round' 27099 * }; 27100 * 27101 * // Complex stroke settings 27102 * graphics.strokeStyle = { 27103 * width: 6, 27104 * color: 0x00ff00, 27105 * alpha: 0.5, 27106 * join: 'miter', 27107 * miterLimit: 10, 27108 * }; 27109 * ``` 27110 * @see {@link StrokeStyle} For all available stroke style options 27111 * @see {@link Graphics#stroke} For applying the stroke to paths 27112 */ 27113 get strokeStyle() { 27114 return this._context.strokeStyle; 27115 } 27116 set strokeStyle(value) { 27117 this._context.strokeStyle = value; 27118 } 27119 /** 27120 * Creates a new Graphics object that copies the current graphics content. 27121 * The clone can either share the same context (shallow clone) or have its own independent 27122 * context (deep clone). 27123 * @example 27124 * ```ts 27125 * const graphics = new Graphics(); 27126 * 27127 * // Create original graphics content 27128 * graphics 27129 * .circle(100, 100, 50) 27130 * .fill({ color: 0xff0000 }); 27131 * 27132 * // Create a shallow clone (shared context) 27133 * const shallowClone = graphics.clone(); 27134 * 27135 * // Changes to original affect the clone 27136 * graphics 27137 * .circle(200, 100, 30) 27138 * .fill({ color: 0x00ff00 }); 27139 * 27140 * // Create a deep clone (independent context) 27141 * const deepClone = graphics.clone(true); 27142 * 27143 * // Modify deep clone independently 27144 * deepClone 27145 * .translateTransform(100, 100) 27146 * .circle(0, 0, 40) 27147 * .fill({ color: 0x0000ff }); 27148 * ``` 27149 * @param deep - Whether to create a deep clone of the graphics object. 27150 * If false (default), the context will be shared between objects. 27151 * If true, creates an independent copy of the context. 27152 * @returns A new Graphics instance with either shared or copied context 27153 * @see {@link Graphics#context} For accessing the underlying graphics context 27154 * @see {@link GraphicsContext} For understanding the shared context behavior 27155 */ 27156 clone(deep = false) { 27157 if (deep) { 27158 return new Graphics(this._context.clone()); 27159 } 27160 this._ownedContext = null; 27161 const clone = new Graphics(this._context); 27162 return clone; 27163 } 27164 // -------- v7 deprecations --------- 27165 /** 27166 * @param width 27167 * @param color 27168 * @param alpha 27169 * @deprecated since 8.0.0 Use {@link Graphics#setStrokeStyle} instead 27170 */ 27171 lineStyle(width, color, alpha) { 27172 deprecation(v8_0_0, "Graphics#lineStyle is no longer needed. Use Graphics#setStrokeStyle to set the stroke style."); 27173 const strokeStyle = {}; 27174 width && (strokeStyle.width = width); 27175 color && (strokeStyle.color = color); 27176 alpha && (strokeStyle.alpha = alpha); 27177 this.context.strokeStyle = strokeStyle; 27178 return this; 27179 } 27180 /** 27181 * @param color 27182 * @param alpha 27183 * @deprecated since 8.0.0 Use {@link Graphics#fill} instead 27184 */ 27185 beginFill(color, alpha) { 27186 deprecation(v8_0_0, "Graphics#beginFill is no longer needed. Use Graphics#fill to fill the shape with the desired style."); 27187 const fillStyle = {}; 27188 if (color !== void 0)
27188 27189 fillStyle.color = color; 27190 if (alpha !== void 0) 27191 fillStyle.alpha = alpha; 27192 this.context.fillStyle = fillStyle; 27193 return this; 27194 } 27195 /** 27196 * @deprecated since 8.0.0 Use {@link Graphics#fill} instead 27197 */ 27198 endFill() { 27199 deprecation(v8_0_0, "Graphics#endFill is no longer needed. Use Graphics#fill to fill the shape with the desired style."); 27200 this.context.fill(); 27201 const strokeStyle = this.context.strokeStyle; 27202 if (strokeStyle.width !== GraphicsContext.defaultStrokeStyle.width || strokeStyle.color !== GraphicsContext.defaultStrokeStyle.color || strokeStyle.alpha !== GraphicsContext.defaultStrokeStyle.alpha) { 27203 this.context.stroke(); 27204 } 27205 return this; 27206 } 27207 /** 27208 * @param {...any} args 27209 * @deprecated since 8.0.0 Use {@link Graphics#circle} instead 27210 */ 27211 drawCircle(...args) { 27212 deprecation(v8_0_0, "Graphics#drawCircle has been renamed to Graphics#circle"); 27213 return this._callContextMethod("circle", args); 27214 } 27215 /** 27216 * @param {...any} args 27217 * @deprecated since 8.0.0 Use {@link Graphics#ellipse} instead 27218 */ 27219 drawEllipse(...args) { 27220 deprecation(v8_0_0, "Graphics#drawEllipse has been renamed to Graphics#ellipse"); 27221 return this._callContextMethod("ellipse", args); 27222 } 27223 /** 27224 * @param {...any} args 27225 * @deprecated since 8.0.0 Use {@link Graphics#poly} instead 27226 */ 27227 drawPolygon(...args) { 27228 deprecation(v8_0_0, "Graphics#drawPolygon has been renamed to Graphics#poly"); 27229 return this._callContextMethod("poly", args); 27230 } 27231 /** 27232 * @param {...any} args 27233 * @deprecated since 8.0.0 Use {@link Graphics#rect} instead 27234 */ 27235 drawRect(...args) { 27236 deprecation(v8_0_0, "Graphics#drawRect has been renamed to Graphics#rect"); 27237 return this._callContextMethod("rect", args); 27238 } 27239 /** 27240 * @param {...any} args 27241 * @deprecated since 8.0.0 Use {@link Graphics#roundRect} instead 27242 */ 27243 drawRoundedRect(...args) { 27244 deprecation(v8_0_0, "Graphics#drawRoundedRect has been renamed to Graphics#roundRect"); 27245 return this._callContextMethod("roundRect", args); 27246 } 27247 /** 27248 * @param {...any} args 27249 * @deprecated since 8.0.0 Use {@link Graphics#star} instead 27250 */ 27251 drawStar(...args) { 27252 deprecation(v8_0_0, "Graphics#drawStar has been renamed to Graphics#star"); 27253 return this._callContextMethod("star", args); 27254 } 27255} 27256 27257class RenderTexture extends Texture { 27258 /** 27259 * Creates a RenderTexture. Pass `dynamic: true` in options to allow resizing after creation. 27260 * @param options - Options for the RenderTexture, including width, height, and dynamic. 27261 * @returns A new RenderTexture instance. 27262 * @example 27263 * const rt = RenderTexture.create({ width: 100, height: 100, dynamic: true }); 27264 * rt.resize(500, 500); 27265 */ 27266 static create(options) { 27267 const { dynamic, ...rest } = options; 27268 return new RenderTexture({ 27269 source: new TextureSource(rest), 27270 dynamic: dynamic ?? false 27271 }); 27272 } 27273 /** 27274 * Resizes the render texture. 27275 * @param width - The new width of the render texture. 27276 * @param height - The new height of the render texture. 27277 * @param resolution - The new resolution of the render texture. 27278 * @returns This texture. 27279 */ 27280 resize(width, height, resolution) { 27281 this.source.resize(width, height, resolution); 27282 return this; 27283 } 27284} 27285 27286var DEPRECATED_WRAP_MODES = /* @__PURE__ */ ((DEPRECATED_WRAP_MODES2) => { 27287 DEPRECATED_WRAP_MODES2["CLAMP"] = "clamp-to-edge"; 27288 DEPRECATED_WRAP_MODES2["REPEAT"] = "repeat"; 27289 DEPRECATED_WRAP_MODES2["MIRRORED_REPEAT"] = "mirror-repeat"; 27290 return DEPRECATED_WRAP_MODES2; 27291})(DEPRECATED_WRAP_MODES || {}); 27292new Proxy(DEPRECATED_WRAP_MODES, {
27293 get(target, prop) { 27294 deprecation(v8_0_0, `DRAW_MODES.${prop} is deprecated, use '${DEPRECATED_WRAP_MODES[prop]}' instead`); 27295 return target[prop]; 27296 } 27297}); 27298var DEPRECATED_SCALE_MODES = /* @__PURE__ */ ((DEPRECATED_SCALE_MODES2) => { 27299 DEPRECATED_SCALE_MODES2["NEAREST"] = "nearest"; 27300 DEPRECATED_SCALE_MODES2["LINEAR"] = "linear"; 27301 return DEPRECATED_SCALE_MODES2; 27302})(DEPRECATED_SCALE_MODES || {}); 27303const SCALE_MODES = new Proxy(DEPRECATED_SCALE_MODES, { 27304 get(target, prop) { 27305 deprecation(v8_0_0, `DRAW_MODES.${prop} is deprecated, use '${DEPRECATED_SCALE_MODES[prop]}' instead`); 27306 return target[prop]; 27307 } 27308}); 27309 27310extensions.add(browserExt, webworkerExt); 27311 27312function clamp(n, min, max) { 27313 return n < min ? min : n > max ? max : n; 27314} 27315function get_canvas_blob(renderer, obj, bounds) { 27316 return new Promise((resolve) => { 27317 if (!obj) { 27318 resolve(null); 27319 return; 27320 } 27321 const image_bounds = obj.getLocalBounds(); 27322 const frame = bounds ? new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height) : new Rectangle(0, 0, image_bounds.width, image_bounds.height); 27323 const src_canvas = renderer.extract.canvas({ 27324 target: obj, 27325 resolution: 1, 27326 frame 27327 }); 27328 src_canvas.toBlob?.((blob) => { 27329 if (!blob) { 27330 resolve(null); 27331 } 27332 resolve(blob); 27333 }); 27334 }); 27335} 27336 27337var root$6 = from_html(`<div class="color-gradient svelte-qx84u5"><div class="marker svelte-qx84u5"></div></div> <div class="opacity-slider svelte-qx84u5"><div class="opacity-marker svelte-qx84u5"></div></div> <div class="hue-slider svelte-qx84u5"><div class="marker svelte-qx84u5"></div></div>`, 1); 27338 27339function ColorPicker($$anchor, $$props) { 27340 push($$props, false); 27341 27342 let color = prop($$props, 'color', 12, "rgb(255, 255, 255)"); 27343 let sl_marker_pos = mutable_source([0, 0]); 27344 let sl_rect = null; 27345 let sl_moving = false; 27346 let sl = [0, 0]; 27347 let hue = mutable_source(0); 27348 let hue_marker_pos = mutable_source(0); 27349 let hue_rect = null; 27350 let hue_moving = false; 27351 let opacity_moving = false; 27352 27353 function handle_hue_down(event) { 27354 hue_rect = event.currentTarget.getBoundingClientRect(); 27355 hue_moving = true; 27356 update_hue_from_mouse(event.clientX); 27357 } 27358 27359 function update_hue_from_mouse(x) { 27360 if (!hue_rect) return; 27361 27362 const _x = clamp(x - hue_rect.left, 0, hue_rect.width); // get the x-coordinate relative to the box 27363 27364 set(hue_marker_pos, _x); 27365 27366 const _hue = _x / hue_rect.width * 360; // scale the x position to a hue value (0-360) 27367 27368 set(hue, _hue); 27369 color(hsva_to_rgba({ h: _hue, s: sl[0], v: sl[1], a: 1 })); 27370 } 27371 27372 function hsva_to_rgba(hsva) { 27373 const saturation = hsva.s; 27374 const value = hsva.v; 27375 let chroma = saturation * value; 27376 const hue_by_60 = hsva.h / 60; 27377 let x = chroma * (1 - Math.abs(hue_by_60 % 2 - 1)); 27378 const m = value - chroma; 27379 27380 chroma = chroma + m; 27381 x = x + m; 27382 27383 const index = Math.floor(hue_by_60) % 6; 27384 const red = [chroma, x, m, m, x, chroma][index]; 27385 const green = [x, chroma, chroma, x, m, m][index]; 27386 const blue = [m, m, x, chroma, chroma, x][index]; 27387 27388 return `rgba(${red * 255}, ${green * 255}, ${blue * 255}, ${hsva.a}
27388)`; 27389 } 27390 27391 function update_color_from_mouse(x, y) { 27392 if (!sl_rect) return; 27393 27394 const _x = clamp(x - sl_rect.left, 0, sl_rect.width); 27395 const _y = clamp(y - sl_rect.top, 0, sl_rect.height); 27396 27397 set(sl_marker_pos, [_x, _y]); 27398 27399 const _hsva = { 27400 h: get(hue) * 1, 27401 s: _x / sl_rect.width, 27402 v: 1 - _y / sl_rect.height, 27403 a: 1 27404 }; 27405 27406 sl = [_hsva.s, _hsva.v]; 27407 color(hsva_to_rgba(_hsva)); 27408 } 27409 27410 function handle_sl_down(event) { 27411 sl_moving = true; 27412 sl_rect = event.currentTarget.getBoundingClientRect(); 27413 update_color_from_mouse(event.clientX, event.clientY); 27414 } 27415 27416 function handle_move(event) { 27417 if (sl_moving) update_color_from_mouse(event.clientX, event.clientY); 27418 if (hue_moving) update_hue_from_mouse(event.clientX); 27419 if (opacity_moving) update_opacity_from_mouse(event.clientX); 27420 } 27421 27422 function handle_end() { 27423 if (sl_moving) sl_moving = false; 27424 if (hue_moving) hue_moving = false; 27425 if (opacity_moving) opacity_moving = false; 27426 } 27427 27428 async function update_mouse_from_color(color) { 27429 if (sl_moving || hue_moving) return; 27430 27431 await tick(); 27432 27433 if (!color) return; 27434 27435 if (!sl_rect) { 27436 sl_rect = get(sl_wrap).getBoundingClientRect(); 27437 } 27438 27439 if (!hue_rect) { 27440 hue_rect = get(hue_wrap).getBoundingClientRect(); 27441 } 27442 27443 const hsva = tinycolor(color).toHsv(); 27444 const _x = hsva.s * sl_rect.width;
27445 const _y = (1 - hsva.v) * sl_rect.height; 27446 27447 set(sl_marker_pos, [_x, _y]); 27448 sl = [hsva.s, hsva.v]; 27449 set(hue, hsva.h); 27450 set(hue_marker_pos, hsva.h / 360 * hue_rect.width); 27451 } 27452 27453 let sl_wrap = mutable_source(); 27454 let hue_wrap = mutable_source(); 27455 let opacity = prop($$props, 'opacity', 12, 1); 27456 let opacity_wrap = mutable_source(); 27457 27458 function handle_opacity_down(event) { 27459 set(opacity_rect, event.currentTarget.getBoundingClientRect()); 27460 opacity_moving = true; 27461 update_opacity_from_mouse(event.clientX); 27462 } 27463 27464 let opacity_rect = mutable_source(null); 27465 27466 function update_opacity_from_mouse(x) { 27467 if (!get(opacity_rect)) return; 27468 27469 const _x = clamp(x - get(opacity_rect).left, 0, get(opacity_rect).width); 27470 27471 opacity(_x / get(opacity_rect).width); 27472 } 27473 27474 onMount(() => { 27475 set(opacity_rect, get(opacity_wrap).getBoundingClientRect()); 27476 }); 27477 27478 legacy_pre_effect(() => (deep_read_state(color())), () => { 27479 update_mouse_from_color(color()); 27480 }); 27481 27482 legacy_pre_effect_reset(); 27483 27484 var $$exports = { 27485 get color() { 27486 return color(); 27487 }, 27488 27489 set color($$value) { 27490 color($$value); 27491 flushSync(); 27492 }, 27493 27494 get opacity() { 27495 return opacity(); 27496 }, 27497 27498 set opacity($$value) { 27499 opacity($$value); 27500 flushSync(); 27501 } 27502 }; 27503 27504 init$1(); 27505 27506 var fragment = root$6(); 27507 27508 event('mousemove', $window, handle_move); 27509 event('mouseup', $window, handle_end); 27510 27511 var div = first_child(fragment); 27512 var div_1 = child(div); 27513 let styles; 27514 27515 reset(div); 27516 bind_this(div, ($$value) => set(sl_wrap, $$value), () => get(sl_wrap)); 27517 27518 var div_2 = sibling(div, 2); 27519 var div_3 = child(div_2); 27520 let styles_1; 27521 27522 reset(div_2); 27523 bind_this(div_2, ($$value) => set(opacity_wrap, $$value), () => get(opacity_wrap)); 27524 27525 var div_4 = sibling(div_2, 2); 27526 var div_5 = child(div_4); 27527 let styles_2; 27528 27529 reset(div_4); 27530 bind_this(div_4, ($$value) => set(hue_wrap, $$value), () => get(hue_wrap)); 27531 27532 template_effect(() => { 27533 set_style(div, `--hue:${get(hue) ?? ''}`); 27534 27535 styles = set_style(div_1, '', styles, { 27536 transform: `translate(${( 27537 get(sl_marker_pos), 27538 untrack(() => get(sl_marker_pos)[0]) 27539 ) ?? ''}px,${( 27540 get(sl_marker_pos), 27541 untrack(() => get(sl_marker_pos)[1]) 27542 ) ?? ''}px)`, 27543 background: color() 27544 }); 27545 27546 set_style(div_2, `--color:${(color() === 'auto' ? 'transparent' : color()) ?? ''}`); 27547 27548 styles_1 = set_style(div_3, '', styles_1, { 27549 background: 'white', 27550 transform: `translateX(${( 27551 deep_read_state(opacity()), 27552 get(opacity_rect), 27553 untrack(() => opacity() * (get(opacity_rect)?.width ?? 1)) 27554 ) ?? ''}px)` 27555 }); 27556 27557 styles_2 = set_style(div_5, '', styles_2, { 27558 background: "hsl(" + get(hue) + ", 100%, 50%)", 27559 transform: `translateX(${get(hue_marker_pos) ?? ''}px)` 27560 }); 27561 }); 27562 27563 event('mousedown', div, handle_sl_down); 27564 event('mousedown', div_2, handle_opacity_down); 27565 event('mousedown', div_4, handle_hue_down); 27566 append($$anchor, fragment); 27567 27568 return pop($$exports); 27569} 27570 27571var root_1$4 = from_html(`<span></span>`); 27572var root_4$2 = from_html(`<button></button>`); 27573var root_2$5 = from_html(`<div class="swatch svelte-q8yof7"><!> <!> <button></button></div>`); 27574var root_5$3 = from_html(`<button></button>`); 27575var root$5 = from_html(`<!> <div class="swatch-wrap svelte-q8yof7"><div class="swatch-container svelte-q8yof7"><!> <menu class="swatch svelte-q8yof7"></menu></div></div>`, 1); 27576 27577function ColorSwatch($$anchor, $$props) { 27578 push($$props, false); 27579 27580 const _colors = mutable_source(); 27581 const selected_opacity = mutable_source(); 27582 let selected_color = prop($$props, 'selected_color', 12); 27583 let colors = prop($$props, 'colors', 12); 27584 let user_colors = prop($$props, 'user_colors', 28, () => []); 27585 let show_empty = prop($$props, 'show_empty', 12, false); 27586 let current_mode = prop($$props, 'current_mode', 12, "hex"); 27587 let color_picker = prop($$props, 'color_picker', 12, false); 27588 const dispatch = createEventDispatcher(); 27589 27590 function get_color(color_input) { 27591 if (Array.isArray(color_input)) { 27592 return color_input[0]; 27593 } 27594 27595 return color_input; 27596 } 27597 27598 function get_opacity(color_input) { 27599 if (Array.isArray(color_input)) { 27600 return color_input[1]; 27601 } 27602 27603 const color = tinycolor(color_input); 27604 27605 return color.getAlpha(); 27606 } 27607 27608 function get_formatted_color(color, mode) { 27609 const color_value = get_color(color); 27610 27611 if (mode === "hex") { 27612 return tinycolor(color_value).toHexString(); 27613 } else if (mode === "rgb") { 27614 return tinycolor(color_value).toRgbString(); 27615 } 27616 27617 return tinycolor(color_value).toHslString(); 27618 } 27619 27620 `select-${colors().findIndex((c) => get_formatted_color(c, current_mode()) === get_formatted_color(selected_color(), current_mode()))}`; 27621 27622 function handle_select(type, detail) { 27623 `${type}-${detail.index}`; 27624 dispatch(type, detail); 27625 } 27626 27627 function handle_picker_click() { 27628 dispatch("select", { index: null, color: selected_color() }); 27629 color_picker(!color_picker()); 27630 } 27631 27632 legacy_pre_effect(() => (deep_read_state(show_empty()), deep_read_state(colors())), () => { 27633 set(_colors, show_empty() ? colors() : colors().filter((c) => c)); 27634 }); 27635 27636 legacy_pre_effect(() => (deep_read_state(selected_color())), () => { 27637 set(selected_opacity, get_opacity(selected_color())); 27638 }); 27639 27640 legacy_pre_effect_reset(); 27641 27642 var $$exports = { 27643 get selected_color() { 27644 return selected_color(); 27645 }, 27646 27647 set selected_color($$value) { 27648 selected_color($$value); 27649 flushSync(); 27650 }, 27651 27652 get colors() { 27653 return colors(); 27654 }, 27655 27656 set colors($$value) { 27657 colors($$value); 27658 flushSync(); 27659 }, 27660 27661 get user_colors() { 27662 return user_colors(); 27663 }, 27664 27665 set user_colors($$value) { 27666 user_colors($$value); 27667 flushSync(); 27668 }, 27669 27670 get show_empty() { 27671 return show_empty(); 27672 }, 27673 27674 set show_empty($$value) { 27675 show_empty($$value); 27676 flushSync(); 27677 }, 27678 27679 get current_mode() { 27680 return current_mode(); 27681 }, 27682 27683 set current_mode($$value) { 27684 current_mode($$value); 27685 flushSync(); 27686 }, 27687 27688 get color_picker() { 27689 return color_picker(); 27690 }, 27691 27692 set color_picker($$value) { 27693 color_picker($$value); 27694 flushSync(); 27695 } 27696 }; 27697 27698 init$1(); 27699 27700 var fragment = root$5(); 27701 var node = first_child(fragment); 27702 27703 { 27704 var consequent = ($$anchor) => { 27705 var span = root_1$4(); 27706 let classes; 27707 27708 template_effect(() => classes = set_class(span, 1, 'svelte-q8yof7', null, classes, { lg: user_colors() })); 27709 append($$anchor, span); 27710 }; 27711 27712 if_block(node, ($$render) => { 27713 if (!color_picker()) $$render(consequent); 27714 }); 27715 } 27716 27717 var div = sibling(node, 2); 27718 var div_1 = child(div);
27719 var node_1 = child(div_1); 27720 27721 { 27722 var consequent_2 = ($$anchor) => { 27723 var div_2 = root_2$5(); 27724 var node_2 = child(div_2); 27725 27726 { 27727 var consequent_1 = ($$anchor) => { 27728 { 27729 let $0 = derived_safe_equal(() => ( 27730 deep_read_state(selected_color()), 27731 untrack(() => get_color(selected_color())) 27732 )); 27733 27734 IconButton($$anchor, { 27735 get Icon() { 27736 return Plus; 27737 }, 27738 onclick: () => dispatch("add_color"), 27739 roundedness: 'very', 27740 get background() { 27741 return get($0); 27742 }, 27743 color: 'white' 27744 }); 27745 } 27746 }; 27747 27748 if_block(node_2, ($$render) => { 27749 if (color_picker()) $$render(consequent_1); 27750 }); 27751 } 27752 27753 var node_3 = sibling(node_2, 2); 27754 27755 each(node_3, 1, user_colors, index, ($$anchor, color, i) => { 27756 const color_string = derived_safe_equal(() => (get(color), untrack(() => get_color(get(color))))); 27757 const opacity = derived_safe_equal(() => (get(color), untrack(() => get_opacity(get(color))))); 27758 var button = root_4$2(); 27759 let classes_1; 27760 let styles; 27761 27762 template_effect(() => { 27763 classes_1 = set_class(button, 1, 'color svelte-q8yof7', null, classes_1, { 27764 empty: get(color) === null, 27765 selected: `${get(color_string)}-${get(opacity)}` === `${selected_color()}-${get(selected_opacity)}` 27766 }); 27767 27768 styles = set_style(button, `background-color: ${get(color_string) ?? ''}`, styles, { opacity: get(opacity) }); 27769 }); 27770 27771 event('click', button, () => handle_select("edit", { 27772 index: i, 27773 color: get(color_string), 27774 opacity: get(opacity) 27775 })); 27776 27777 append($$anchor, button); 27778 }); 27779 27780 var button_1 = sibling(node_3, 2); 27781 let classes_2; 27782 27783 reset(div_2); 27784 template_effect(() => classes_2 = set_class(button_1, 1, 'color colorpicker svelte-q8yof7', null, classes_2, { hidden: !color_picker() })); 27785 event('click', button_1, handle_picker_click); 27786 append($$anchor, div_2); 27787 }; 27788 27789 if_block(node_1, ($$render) => { 27790 if (user_colors()) $$render(consequent_2); 27791 }); 27792 } 27793 27794 var menu = sibling(node_1, 2); 27795 27796 each(menu, 5, () => get(_colors), index, ($$anchor, color_item, i) => { 27797 const color_string = derived_safe_equal(() => ( 27798 get(color_item), 27799 untrack(() => get_color(get(color_item))) 27800 )); 27801 27802 const opacity = derived_safe_equal(() => ( 27803 get(color_item), 27804 untrack(() => get_opacity(get(color_item))) 27805 )); 27806 27807 var button_2 = root_5$3(); 27808 let classes_3; 27809 let styles_1; 27810 27811 template_effect(() => { 27812 classes_3 = set_class(button_2, 1, 'color svelte-q8yof7', null, classes_3, { 27813 empty: get(color_item) === null, 27814 selected: `${get(color_string)}-${get(opacity)}` === `${selected_color()}-${get(selected_opacity)}` 27815 }); 27816 27817 styles_1 = set_style(button_2, `background-color: ${get(color_string) ?? ''}`, styles_1, { opacity: get(opacity) }); 27818 }); 27819 27820 event('click', button_2, () => handle_select("select", { 27821 index: i, 27822 color: get(color_string), 27823 opacity: get(opacity) 27824 })); 27825 27826 append($$anchor, button_2); 27827 }); 27828 27829 reset(menu); 27830 reset(div_1); 27831 reset(div); 27832 append($$anchor, fragment); 27833 27834 return pop($$exports); 27835} 27836 27837var root_2$4 = from_html(`<button> </button>`); 27838var root$4 = from_html(`<div class="input svelte-yn52fn"><button class="swatch svelte-yn52fn"></button> <div><div class="input-wrap svelte-yn52fn"><input type="text" class="svelte-yn52fn"/> <button class="eyedropper svelte-yn52fn"><!></button></div> <div class="buttons svelte-yn52fn"></div></div></div>`); 27839 27840function ColorField($$anchor, $$props) { 27841 push($$props, false); 27842 27843 const color_string = mutable_source(); 27844 let color = prop($$props, 'color', 12); 27845 let current_mode = prop($$props, 'current_mode', 12, "hex"); 27846 const dispatch = createEventDispatcher(); 27847 const modes = [["Hex", "hex"], ["RGB", "rgb"], ["HSL", "hsl"]]; 27848 27849 function format_color(color, mode) { 27850 if (mode === "hex") { 27851 return tinycolor(color).toHexString(); 27852 } else if (mode === "rgb") { 27853 return tinycolor(color).toRgbString(); 27854 } 27855 27856 return tinycolor(color).toHslString(); 27857 } 27858 27859 function request_eyedropper() { 27860 // @ts-ignore 27861 const eyeDropper = new EyeDropper(); 27862 27863 eyeDropper.open().then((result) => { 27864 color(result.sRGBHex); 27865 }); 27866 } 27867 27868 //@ts-ignore 27869 const eyedropper_supported = !!window.EyeDropper; 27870 27871 function handle_click() { 27872 dispatch("selected", get(color_string)); 27873 dispatch("close"); 27874 } 27875 27876 legacy_pre_effect( 27877 () => ( 27878 deep_read_state(color()), 27879 deep_read_state(current_mode()) 27880 ), 27881 () => { 27882 set(color_string, format_color(color(), current_mode())); 27883 } 27884 ); 27885 27886 legacy_pre_effect(() => (get(color_string)), () => { 27887 get(color_string) && dispatch("selected", get(color_string)); 27888 }); 27889 27890 legacy_pre_effect_reset(); 27891 27892 var $$exports = { 27893 get color() { 27894 return color(); 27895 }, 27896 27897 set color($$value) { 27898 color($$value); 27899 flushSync(); 27900 }, 27901 27902 get current_mode() { 27903 return current_mode(); 27904 }, 27905 27906 set current_mode($$value) { 27907 current_mode($$value); 27908 flushSync(); 27909 } 27910 }; 27911 27912 init$1(); 27913 27914 var div = root$4(); 27915 var button = child(div); 27916 let styles; 27917 var div_1 = sibling(button, 2); 27918 var div_2 = child(div_1); 27919 var input = child(div_2); 27920 27921 remove_input_defaults(input); 27922 27923 var button_1 = sibling(input, 2); 27924 var node = child(button_1); 27925 27926 { 27927 var consequent = ($$anchor) => { 27928 Eyedropper($$anchor); 27929 }; 27930 27931 if_block(node, ($$render) => { 27932 if (eyedropper_supported) $$render(consequent); 27933 }); 27934 } 27935 27936 reset(button_1); 27937 reset(div_2); 27938 27939 var div_3 = sibling(div_2, 2); 27940 27941 each(div_3, 5, () => modes, index, ($$anchor, $$item) => { 27942 var $$array = user_derived(() => to_array(get($$item), 2)); 27943 let label = () => get($$array)[0]; 27944 let value = () => get($$array)[1]; 27945 var button_2 = root_2$4(); 27946 let classes; 27947 var text = child(button_2, true); 27948 27949 reset(button_2); 27950 27951 template_effect(() => { 27952 classes = set_class(button_2, 1, 'button svelte-yn52fn', null, classes, { active: current_mode() === value() }); 27953 set_text(text, label()); 27954 }); 27955 27956 event('click', button_2, () => current_mode(value())); 27957 append($$anchor, button_2); 27958 }); 27959 27960 reset(div_3); 27961 reset(div_1); 27962 reset(div); 27963 27964 template_effect(() => { 27965 styles = set_style(button, '', styles, { background: color() }); 27966 set_value(input, get(color_string)); 27967 }); 27968 27969 event('click', button, handle_click);
27970 event('change', input, (e) => color(e.currentTarget.value)); 27971 event('click', button_1, request_eyedropper); 27972 append($$anchor, div); 27973 27974 return pop($$exports); 27975} 27976 27977var root$3 = from_html(`<div class="wrap svelte-1y25b79"><span class="svelte-1y25b79"><!></span> <input type="range" class="svelte-1y25b79"/></div>`); 27978 27979function BrushSize_1($$anchor, $$props) { 27980 push($$props, false); 27981 27982 let selected_size = prop($$props, 'selected_size', 12); 27983 let min = prop($$props, 'min', 12); 27984 let max = prop($$props, 'max', 12); 27985 const dispatch = createEventDispatcher(); 27986 27987 var $$exports = { 27988 get selected_size() { 27989 return selected_size(); 27990 }, 27991 27992 set selected_size($$value) { 27993 selected_size($$value); 27994 flushSync(); 27995 }, 27996 27997 get min() { 27998 return min(); 27999 }, 28000 28001 set min($$value) { 28002 min($$value); 28003 flushSync(); 28004 }, 28005 28006 get max() { 28007 return max(); 28008 }, 28009 28010 set max($$value) { 28011 max($$value); 28012 flushSync(); 28013 } 28014 }; 28015 28016 init$1(); 28017 28018 var div = root$3(); 28019 var span = child(div); 28020 var node = child(span); 28021 28022 BrushSize(node); 28023 reset(span); 28024 28025 var input = sibling(span, 2); 28026 28027 remove_input_defaults(input); 28028 set_attribute(input, 'step', 1); 28029 reset(div); 28030 action(div, ($$node, $$action_arg) => click_outside?.($$node, $$action_arg), () => () => dispatch("click_outside")); 28031 28032 template_effect(() => { 28033 set_attribute(input, 'min', min()); 28034 set_attribute(input, 'max', max()); 28035 }); 28036 28037 bind_value(input, selected_size); 28038 append($$anchor, div); 28039 28040 return pop($$exports); 28041} 28042 28043var root_2$3 = from_html(`<!> <!>`, 1); 28044var root$2 = from_html(`<div><!> <!> <!></div>`); 28045 28046function BrushOptions($$anchor, $$props) { 28047 push($$props, false); 28048 28049 let colors = prop($$props, 'colors', 12); 28050 let selected_color = prop($$props, 'selected_color', 12); 28051 let color_mode = prop($$props, 'color_mode', 12, undefined); 28052 let recent_colors = prop($$props, 'recent_colors', 28, () => []); 28053 let selected_size = prop($$props, 'selected_size', 12); 28054 let selected_opacity = prop($$props, 'selected_opacity', 12); 28055 let show_swatch = prop($$props, 'show_swatch', 12); 28056 let show_size = prop($$props, 'show_size', 12); 28057 let mode = prop($$props, 'mode', 12, "brush"); 28058 let color_picker = mutable_source(false); 28059 let current_mode = mutable_source("hex"); 28060 let editing_index = null; 28061 const dispatch = createEventDispatcher(); 28062 28063 function handle_color_selection({ index, color, opacity }, type) { 28064 if (type === "user" && !color) { 28065 editing_index = index; 28066 set(color_picker, true); 28067 } 28068 28069 if (!color) return; 28070 28071 selected_color(color); 28072 28073 if (opacity !== undefined) { 28074 selected_opacity(opacity); 28075 } 28076 28077 if (type === "core") { 28078 set(color_picker, false); 28079 } 28080 } 28081 28082 function handle_color_change(color) { 28083 if (editing_index === null) return; 28084 28085 recent_colors(recent_colors()[editing_index] = color, true); 28086 } 28087 28088 let width = mutable_source(0); 28089 let height = mutable_source(0); 28090 28091 function debounce(func, delay) { 28092 let timeout; 28093 28094 return function (...args) { 28095 clearTimeout(timeout); 28096 timeout = setTimeout(() => func(...args), delay); 28097 }; 28098 } 28099 28100 let preview = prop($$props, 'preview', 12, false); 28101 28102 function handle_preview() { 28103 if (!preview()) { 28104 preview(true); 28105 debounced_close_preview(); 28106 } else { 28107 debounced_close_preview(); 28108 } 28109 } 28110 28111 function handle_close_preview() { 28112 preview(false); 28113 } 28114 28115 const debounced_close_preview = debounce(handle_close_preview, 1000); 28116 28117 function handle_select(color) { 28118 selected_color(color); 28119 } 28120 28121 function handle_add_color() { 28122 // limit to 5 28123 if (recent_colors().length >= 5) { 28124 recent_colors().pop(); 28125 } 28126 28127 // check if the color is already in the array 28128 if (recent_colors().some((color) => { 28129 if (Array.isArray(color)) { 28130 return color[0] === selected_color() && color[1] === selected_opacity(); 28131 } 28132 28133 return color === selected_color(); 28134 })) { 28135 return; 28136 } 28137 28138 recent_colors().push([selected_color(), selected_opacity()]); 28139 recent_colors(recent_colors()); 28140 } 28141 28142 legacy_pre_effect(() => (deep_read_state(selected_color())), () => { 28143 handle_color_change(selected_color()); 28144 }); 28145 28146 legacy_pre_effect( 28147 () => ( 28148 deep_read_state(selected_size()), 28149 deep_read_state(selected_color()) 28150 ), 28151 () => { 28152 (selected_size(), selected_color(), handle_preview()); 28153 } 28154 ); 28155 28156 legacy_pre_effect_reset(); 28157 28158 var $$exports = { 28159 get colors() { 28160 return colors(); 28161 }, 28162 28163 set colors($$value) { 28164 colors($$value); 28165 flushSync(); 28166 }, 28167 28168 get selected_color() { 28169 return selected_color(); 28170 }, 28171 28172 set selected_color($$value) { 28173 selected_color($$value); 28174 flushSync(); 28175 }, 28176 28177 get color_mode() { 28178 return color_mode(); 28179 }, 28180 28181 set color_mode($$value) { 28182 color_mode($$value); 28183 flushSync(); 28184 }, 28185 28186 get recent_colors() { 28187 return recent_colors(); 28188 }, 28189 28190 set recent_colors($$value) { 28191 recent_colors($$value); 28192 flushSync(); 28193 }, 28194 28195 get selected_size() { 28196 return selected_size(); 28197 }, 28198 28199 set selected_size($$value) { 28200 selected_size($$value); 28201 flushSync(); 28202 }, 28203 28204 get selected_opacity() { 28205 return selected_opacity(); 28206 }, 28207 28208 set selected_opacity($$value) { 28209 selected_opacity($$value); 28210 flushSync(); 28211 }, 28212 28213 get show_swatch() { 28214 return show_swatch(); 28215 }, 28216 28217 set show_swatch($$value) { 28218 show_swatch($$value); 28219 flushSync(); 28220 }, 28221 28222 get show_size() { 28223 return show_size(); 28224 }, 28225 28226 set show_size($$value) { 28227 show_size($$value); 28228 flushSync(); 28229 }, 28230 28231 get mode() { 28232 return mode(); 28233 }, 28234 28235 set mode($$value) { 28236 mode($$value); 28237 flushSync(); 28238 }, 28239 28240 get preview() { 28241 return preview(); 28242 }, 28243 28244 set preview($$value) { 28245 preview($$value); 28246 flushSync(); 28247 } 28248 }; 28249 28250 init$1(); 28251 28252 var div = root$2(); 28253 let classes; 28254 var node = child(div); 28255 28256 { 28257 var consequent_1 = ($$anchor) => { 28258 var fragment = comment();
28259 var node_1 = first_child(fragment); 28260 28261 { 28262 var consequent = ($$anchor) => { 28263 var fragment_1 = root_2$3(); 28264 var node_2 = first_child(fragment_1); 28265 28266 ColorPicker(node_2, { 28267 get color() { 28268 return selected_color(); 28269 }, 28270 28271 set color($$value) { 28272 selected_color($$value); 28273 }, 28274 28275 get opacity() { 28276 return selected_opacity(); 28277 }, 28278 28279 set opacity($$value) { 28280 selected_opacity($$value); 28281 }, 28282 $$legacy: true 28283 }); 28284 28285 var node_3 = sibling(node_2, 2); 28286 28287 ColorField(node_3, { 28288 get color() { 28289 return selected_color(); 28290 }, 28291 28292 get current_mode() { 28293 return get(current_mode); 28294 }, 28295 28296 set current_mode($$value) { 28297 set(current_mode, $$value); 28298 }, 28299 28300 $$events: { 28301 close: () => set(color_picker, false), 28302 selected: ({ detail }) => handle_select(detail) 28303 }, 28304 $$legacy: true 28305 }); 28306 28307 append($$anchor, fragment_1); 28308 }; 28309 28310 if_block(node_1, ($$render) => { 28311 if (get(color_picker)) $$render(consequent); 28312 }); 28313 } 28314 28315 append($$anchor, fragment); 28316 }; 28317 28318 if_block(node, ($$render) => { 28319 if (color_mode() === "defaults") $$render(consequent_1); 28320 }); 28321 } 28322 28323 var node_4 = sibling(node, 2); 28324 28325 { 28326 var consequent_2 = ($$anchor) => { 28327 { 28328 let $0 = derived_safe_equal(() => color_mode() === "defaults" ? recent_colors() : null); 28329 28330 ColorSwatch($$anchor, { 28331 get colors() { 28332 return colors(); 28333 }, 28334 28335 get user_colors() { 28336 return get($0); 28337 }, 28338 28339 get selected_color() { 28340 return selected_color(); 28341 }, 28342 28343 get current_mode() { 28344 return get(current_mode); 28345 }, 28346 28347 get color_picker() { 28348 return get(color_picker); 28349 }, 28350 28351 set color_picker($$value) { 28352 set(color_picker, $$value); 28353 }, 28354 28355 $$events: { 28356 select: ({ detail }) => handle_color_selection(detail, "core"), 28357 edit: ({ detail }) => handle_color_selection(detail, "user"), 28358 add_color: handle_add_color 28359 }, 28360 $$legacy: true 28361 }); 28362 } 28363 }; 28364 28365 if_block(node_4, ($$render) => { 28366 if (show_swatch()) $$render(consequent_2); 28367 }); 28368 } 28369 28370 var node_5 = sibling(node_4, 2); 28371 28372 { 28373 var consequent_3 = ($$anchor) => { 28374 BrushSize_1($$anchor, { 28375 max: 100, 28376 min: 1, 28377 get selected_size() { 28378 return selected_size(); 28379 }, 28380 28381 set selected_size($$value) { 28382 selected_size($$value); 28383 }, 28384 $$legacy: true 28385 }); 28386 }; 28387 28388 if_block(node_5, ($$render) => { 28389 if (show_size()) $$render(consequent_3); 28390 }); 28391 } 28392 28393 reset(div); 28394 action(div, ($$node, $$action_arg) => click_outside?.($$node, $$action_arg), () => () => dispatch("click_outside")); 28395 28396 template_effect(() => classes = set_class(div, 1, 'wrap svelte-1l3f0ck', null, classes, { 28397 padded: !get(color_picker), 28398 color_picker: get(color_picker), 28399 size_picker: show_size() && mode() === "brush", 28400 eraser_picker: mode() === "eraser" 28401 })); 28402 28403 bind_window_size('innerHeight', ($$value) => set(height, $$value)); 28404 bind_window_size('innerWidth', ($$value) => set(width, $$value)); 28405 append($$anchor, div); 28406 28407 return pop($$exports); 28408} 28409 28410var root_5$2 = from_html(`<!> <!>`, 1); 28411var root_8$1 = from_html(`<!> <!> <!>`, 1); 28412var root_12 = from_html(`<!> <!> <!>`, 1); 28413var root$1 = from_html(`<div class="toolbar-wrap svelte-1uf24la"><div class="half-container svelte-1uf24la"><!> <!> <!></div> <div><!> <!> <!> <!></div></div>`); 28414 28415function Toolbar($$anchor, $$props) { 28416 push($$props, false); 28417 28418 const can_crop = mutable_source(); 28419 const can_resize = mutable_source(); 28420 const can_upload = mutable_source(); 28421 const can_webcam = mutable_source(); 28422 const can_paste = mutable_source(); 28423 let tool = prop($$props, 'tool', 12, "image"); 28424 let subtool = prop($$props, 'subtool', 12, null); 28425 let background = prop($$props, 'background', 12, false); 28426 let brush_options = prop($$props, 'brush_options', 12); 28427 let selected_size = prop($$props, 'selected_size', 28, () => brush_options() && typeof brush_options().default_size === "number" ? brush_options().default_size : 25); 28428 let eraser_options = prop($$props, 'eraser_options', 12); 28429 let selected_eraser_size = prop($$props, 'selected_eraser_size', 28, () => eraser_options() && typeof eraser_options().default_size === "number" ? eraser_options().default_size : 25); 28430 28431 // Handle default_color including potential color-opacity tuple 28432 let selected_color = prop($$props, 'selected_color', 28, () => brush_options() && (() => { 28433 const default_color = brush_options().default_color; 28434 28435 if (Array.isArray(default_color)) { 28436 return default_color[0]; 28437 } 28438 28439 return default_color; 28440 })()); 28441 28442 // Set default opacity based on default_color if it's a tuple 28443 let selected_opacity = prop($$props, 'selected_opacity', 28, () => brush_options() && (() => { 28444 const default_color = brush_options().default_color; 28445 28446 if (Array.isArray(default_color)) { 28447 return default_color[1]; 28448 } 28449 28450 // Check if color string has opacity 28451 const color = tinycolor(default_color); 28452 28453 if (color.getAlpha() < 1) { 28454 return color.getAlpha(); 28455 } 28456 28457 return 1; 28458 })()); 28459 28460 let preview = prop($$props, 'preview', 12, false); 28461 let show_brush_color = prop($$props, 'show_brush_color', 12, false); 28462 let show_brush_size = prop($$props, 'show_brush_size', 12, false); 28463 let show_eraser_size = prop($$props, 'show_eraser_size', 12, false); 28464 let sources = prop($$props, 'sources', 12); 28465 let transforms = prop($$props, 'transforms', 12); 28466 let recent_colors = []; 28467 const dispatch = createEventDispatcher(); 28468 28469 /** 28470 * Handles tool click events 28471 * @param {Tool} _tool - The selected tool 28472 */ 28473 function handle_tool_click(e, _tool) { 28474 e.stopPropagation(); 28475 dispatch("tool_change", { tool: _tool }); 28476 } 28477 28478 /** 28479 * Handles subtool click events 28480 * @param {Event} e - The click event 28481 * @param {Subtool} _subtool - The selected subtool 28482 */ 28483 function handle_subtool_click(e, _subtool) { 28484 e.stopPropagation(); 28485 dispatch("subtool_change", { tool: tool(), subtool: _subtool }); 28486 } 28487 28488 legacy_pre_effect(() => (deep_read_state(tool()), deep_read_state(subtool())), () => { 28489 show_brush_size(tool() === "draw" && subtool() === "size"); 28490 }); 28491 28492 legacy_pre_effect(() => (deep_read_state(tool()), deep_read_state(subtool())), () => { 28493 show_brush_color(tool() === "draw" && subtool() === "color"); 28494 }); 28495 28496 legacy_pre_effect(() => (deep_read_state(tool()), deep_read_state(subtool())), () => { 28497 show_eraser_size(tool() === "erase" && subtool() === "size"); 28498 }); 28499 28500 legacy_pre_effect(() => (deep_read_state(transforms())), () => { 28501 set(can_crop, transforms().includes("crop")); 28502 }); 28503 28504 legacy_pre_effect(() => (deep_read_state(transforms())), () => { 28505 set(can_resize, transforms().includes("resize")); 28506 }); 28507 28508 legacy_pre_effect(() => (deep_read_state(sources())), () => { 28509 set(can_upload, sources().includes("upload")); 28510 }); 28511 28512 legacy_pre_effect(() => (deep_read_state(sources())), () => { 28513 set(can_webcam, sources().includes("webcam")); 28514 }); 28515 28516 legacy_pre_effect(() => (deep_read_state(sources())), () => { 28517 set(can_paste, sources().includes("clipboard")); 28518 }); 28519 28520 legacy_pre_effect_reset(); 28521 28522 var $$exports = { 28523 get tool() { 28524 return tool(); 28525 }, 28526 28527 set tool($$value) { 28528 tool($$value); 28529 flushSync(); 28530 }, 28531 28532 get subtool() { 28533 return subtool(); 28534 }, 28535 28536 set subtool($$value) { 28537 subtool($$value); 28538 flushSync(); 28539 }, 28540 28541 get background() { 28542 return background(); 28543 }, 28544 28545 set background($$value) { 28546 background($$value); 28547 flushSync(); 28548 }, 28549 28550 get brush_options() { 28551 return brush_options(); 28552 }, 28553 28554 set brush_options($$value) { 28555 brush_options($$value); 28556 flushSync(); 28557 }, 28558 28559 get selected_size() { 28560 return selected_size(); 28561 }, 28562 28563 set selected_size($$value) { 28564 selected_size($$value); 28565 flushSync(); 28566 }, 28567 28568 get eraser_options() { 28569 return eraser_options(); 28570 }, 28571 28572 set eraser_options($$value) { 28573 eraser_options($$value); 28574 flushSync(); 28575 }, 28576 28577 get selected_eraser_size() { 28578 return selected_eraser_size(); 28579 }, 28580 28581 set selected_eraser_size($$value) { 28582 selected_eraser_size($$value); 28583 flushSync(); 28584 }, 28585 28586 get selected_color() { 28587 return selected_color(); 28588 }, 28589 28590 set selected_color($$value) { 28591 selected_color($$value); 28592 flushSync(); 28593 }, 28594 28595 get selected_opacity() { 28596 return selected_opacity(); 28597 }, 28598 28599 set selected_opacity($$value) { 28600 selected_opacity($$value); 28601 flushSync(); 28602 }, 28603 28604 get preview() { 28605 return preview(); 28606 }, 28607 28608 set preview($$value) { 28609 preview($$value); 28610 flushSync(); 28611 }, 28612 28613 get show_brush_color() { 28614 return show_brush_color(); 28615 }, 28616 28617 set show_brush_color($$value) { 28618 show_brush_color($$value); 28619 flushSync(); 28620 }, 28621 28622 get show_brush_size() { 28623 return show_brush_size(); 28624 }, 28625 28626 set show_brush_size($$value) { 28627 show_brush_size($$value); 28628 flushSync(); 28629 }, 28630 28631 get show_eraser_size() { 28632 return show_eraser_size(); 28633 }, 28634 28635 set show_eraser_size($$value) { 28636 show_eraser_size($$value); 28637 flushSync(); 28638 }, 28639 28640 get sources() { 28641 return sources(); 28642 }, 28643 28644 set sources($$value) { 28645 sources($$value); 28646 flushSync(); 28647 }, 28648 28649 get transforms() { 28650 return transforms(); 28651 }, 28652 28653 set transforms($$value) { 28654 transforms($$value); 28655 flushSync(); 28656 } 28657 }; 28658 28659 init$1(); 28660 28661 var div = root$1(); 28662 var div_1 = child(div); 28663 var node = child(div_1); 28664 28665 { 28666 var consequent = ($$anchor) => { 28667 { 28668 let $0 = derived_safe_equal(() => tool() === "image"); 28669 28670 IconButton($$anchor, { 28671 get Icon() { 28672 return Image$1; 28673 }, 28674 label: 'Image', 28675 get highlight() { 28676 return get($0); 28677 }, 28678 onclick: (e) => handle_tool_click(e, "image"), 28679 size: 'medium', 28680 padded: false, 28681 transparent: true 28682 }); 28683 } 28684 }; 28685 28686 if_block(node, ($$render) => { 28687 if (( 28688 deep_read_state(sources()), 28689 untrack(() => sources().length > 0) 28690 )) $$render(consequent); 28691 }); 28692 } 28693
28694 var node_1 = sibling(node, 2); 28695 28696 { 28697 var consequent_1 = ($$anchor) => { 28698 { 28699 let $0 = derived_safe_equal(() => tool() === "draw"); 28700 28701 IconButton($$anchor, { 28702 get Icon() { 28703 return Brush; 28704 }, 28705 label: 'Brush', 28706 onclick: (e) => handle_tool_click(e, "draw"), 28707 get highlight() { 28708 return get($0); 28709 }, 28710 size: 'medium', 28711 padded: false, 28712 transparent: true 28713 }); 28714 } 28715 }; 28716 28717 if_block(node_1, ($$render) => { 28718 if (brush_options()) $$render(consequent_1); 28719 }); 28720 } 28721 28722 var node_2 = sibling(node_1, 2); 28723 28724 { 28725 var consequent_2 = ($$anchor) => { 28726 { 28727 let $0 = derived_safe_equal(() => tool() === "erase"); 28728 28729 IconButton($$anchor, { 28730 get Icon() { 28731 return Erase; 28732 }, 28733 label: 'Erase', 28734 onclick: (e) => handle_tool_click(e, "erase"), 28735 get highlight() { 28736 return get($0); 28737 }, 28738 size: 'medium', 28739 padded: false, 28740 transparent: true 28741 }); 28742 } 28743 }; 28744 28745 if_block(node_2, ($$render) => { 28746 if (eraser_options()) $$render(consequent_2); 28747 }); 28748 } 28749 28750 reset(div_1); 28751 28752 var div_2 = sibling(div_1, 2); 28753 let classes; 28754 var node_3 = child(div_2); 28755 28756 { 28757 var consequent_9 = ($$anchor) => { 28758 var fragment_3 = comment(); 28759 var node_4 = first_child(fragment_3); 28760 28761 { 28762 var consequent_5 = ($$anchor) => { 28763 var fragment_4 = root_5$2(); 28764 var node_5 = first_child(fragment_4); 28765 28766 { 28767 var consequent_3 = ($$anchor) => { 28768 { 28769 let $0 = derived_safe_equal(() => subtool() === "crop"); 28770 28771 IconButton($$anchor, { 28772 get Icon() { 28773 return Crop; 28774 }, 28775 label: 'Crop', 28776 onclick: (e) => handle_subtool_click(e, "crop"), 28777 get highlight() { 28778 return get($0); 28779 }, 28780 size: 'medium', 28781 padded: false, 28782 transparent: true, 28783 offset: 0 28784 }); 28785 } 28786 }; 28787 28788 if_block(node_5, ($$render) => { 28789 if (get(can_crop)) $$render(consequent_3); 28790 }); 28791 } 28792 28793 var node_6 = sibling(node_5, 2); 28794 28795 { 28796 var consequent_4 = ($$anchor) => { 28797 { 28798 let $0 = derived_safe_equal(() => subtool() === "size"); 28799 28800 IconButton($$anchor, { 28801 get Icon() { 28802 return Resize; 28803 }, 28804 label: 'Resize', 28805 onclick: (e) => handle_subtool_click(e, "size"), 28806 get highlight() { 28807 return get($0); 28808 }, 28809 size: 'medium', 28810 padded: false, 28811 transparent: true, 28812 offset: 0 28813 }); 28814 } 28815 }; 28816 28817 if_block(node_6, ($$render) => { 28818 if (get(can_resize)) $$render(consequent_4); 28819 }); 28820 } 28821 28822 append($$anchor, fragment_4); 28823 }; 28824 28825 var alternate = ($$anchor) => { 28826 var fragment_7 = root_8$1(); 28827 var node_7 = first_child(fragment_7); 28828 28829 { 28830 var consequent_6 = ($$anchor) => { 28831 { 28832 let $0 = derived_safe_equal(() => subtool() === "upload"); 28833 28834 IconButton($$anchor, { 28835 get Icon() { 28836 return Upload; 28837 }, 28838 label: 'Upload', 28839 onclick: (e) => handle_subtool_click(e, "upload"), 28840 get highlight() { 28841 return get($0); 28842 }, 28843 size: 'medium', 28844 padded: false, 28845 transparent: true, 28846 offset: 0 28847 }); 28848 } 28849 }; 28850 28851 if_block(node_7, ($$render) => { 28852 if (get(can_upload)) $$render(consequent_6); 28853 }); 28854 } 28855 28856 var node_8 = sibling(node_7, 2); 28857 28858 { 28859 var consequent_7 = ($$anchor) => { 28860 { 28861 let $0 = derived_safe_equal(() => subtool() === "paste"); 28862 28863 IconButton($$anchor, { 28864 get Icon() { 28865 return ImagePaste; 28866 }, 28867 label: 'Paste', 28868 onclick: (e) => handle_subtool_click(e, "paste"), 28869 get highlight() { 28870 return get($0); 28871 }, 28872 size: 'large', 28873 padded: false, 28874 transparent: true, 28875 offset: 0 28876 }); 28877 } 28878 }; 28879 28880 if_block(node_8, ($$render) => { 28881 if (get(can_paste)) $$render(consequent_7); 28882 }); 28883 } 28884 28885 var node_9 = sibling(node_8, 2); 28886 28887 { 28888 var consequent_8 = ($$anchor) => { 28889 { 28890 let $0 = derived_safe_equal(() => subtool() === "webcam"); 28891 28892 IconButton($$anchor, { 28893 get Icon() { 28894 return Webcam; 28895 }, 28896 label: 'Webcam', 28897 onclick: (e) => handle_subtool_click(e, "webcam"), 28898 get highlight() { 28899 return get($0); 28900 }, 28901 size: 'medium', 28902 padded: false, 28903 transparent: true, 28904 offset: 0 28905 }); 28906 } 28907 }; 28908 28909 if_block(node_9, ($$render) => { 28910 if (get(can_webcam)) $$render(consequent_8); 28911 }); 28912 } 28913 28914 append($$anchor, fragment_7); 28915 }; 28916 28917 if_block(node_4, ($$render) => { 28918 if (background()) $$render(consequent_5); else $$render(alternate, false); 28919 }); 28920 } 28921 28922 append($$anchor, fragment_3); 28923 }; 28924 28925 if_block(node_3, ($$render) => { 28926 if (tool() === "image") $$render(consequent_9); 28927 }); 28928 } 28929
28930 var node_10 = sibling(node_3, 2); 28931 28932 { 28933 var consequent_11 = ($$anchor) => { 28934 var fragment_11 = root_12(); 28935 var node_11 = first_child(fragment_11); 28936 28937 IconButton(node_11, { 28938 get Icon() { 28939 return ColorPickerSolid; 28940 }, 28941 label: 'Color', 28942 get color() { 28943 return selected_color(); 28944 }, 28945 onclick: (e) => handle_subtool_click(e, "color"), 28946 size: 'medium', 28947 padded: false, 28948 transparent: true, 28949 offset: 0 28950 }); 28951 28952 var node_12 = sibling(node_11, 2); 28953 28954 { 28955 let $0 = derived_safe_equal(() => subtool() === "size"); 28956 28957 IconButton(node_12, { 28958 get Icon() { 28959 return Circle$1; 28960 }, 28961 label: 'Brush Size', 28962 onclick: (e) => handle_subtool_click(e, "size"), 28963 get highlight() { 28964 return get($0); 28965 }, 28966 size: 'medium', 28967 padded: false, 28968 transparent: true, 28969 offset: 0 28970 }); 28971 } 28972 28973 var node_13 = sibling(node_12, 2); 28974 28975 { 28976 var consequent_10 = ($$anchor) => { 28977 BrushOptions($$anchor, { 28978 get colors() { 28979 return ( 28980 deep_read_state(brush_options()), 28981 untrack(() => brush_options().colors) 28982 ); 28983 }, 28984 28985 get color_mode() { 28986 return ( 28987 deep_read_state(brush_options()), 28988 untrack(() => brush_options().color_mode) 28989 ); 28990 }, 28991 28992 get recent_colors() { 28993 return recent_colors; 28994 }, 28995 28996 get show_swatch() { 28997 return show_brush_color(); 28998 }, 28999 29000 get show_size() { 29001 return show_brush_size(); 29002 }, 29003 mode: 'brush', 29004 get selected_size() { 29005 return selected_size(); 29006 }, 29007 29008 set selected_size($$value) { 29009 selected_size($$value); 29010 }, 29011 29012 get selected_color() { 29013 return selected_color(); 29014 }, 29015 29016 set selected_color($$value) { 29017 selected_color($$value); 29018 }, 29019 29020 get selected_opacity() { 29021 return selected_opacity(); 29022 }, 29023 29024 set selected_opacity($$value) { 29025 selected_opacity($$value); 29026 }, 29027 29028 get preview() { 29029 return preview(); 29030 }, 29031 29032 set preview($$value) { 29033 preview($$value); 29034 }, 29035 29036 $$events: { 29037 click_outside: (e) => { 29038 e.stopPropagation(); 29039 preview(false); 29040 show_brush_color(false); 29041 show_brush_size(false); 29042 handle_subtool_click(e, null); 29043 } 29044 }, 29045 $$legacy: true 29046 }); 29047 }; 29048 29049 if_block(node_13, ($$render) => { 29050 if (show_brush_color() || show_brush_size()) $$render(consequent_10); 29051 }); 29052 } 29053 29054 append($$anchor, fragment_11); 29055 }; 29056 29057 if_block(node_10, ($$render) => { 29058 if (tool() === "draw" && brush_options()) $$render(consequent_11); 29059 }); 29060 } 29061 29062 var node_14 = sibling(node_10, 2); 29063 29064 { 29065 var consequent_12 = ($$anchor) => { 29066 { 29067 let $0 = derived_safe_equal(() => subtool() === "size"); 29068 29069 IconButton($$anchor, { 29070 get Icon() { 29071 return Circle$1; 29072 }, 29073 label: 'Eraser Size', 29074 onclick: (e) => handle_subtool_click(e, "size"), 29075 get highlight() { 29076 return get($0); 29077 }, 29078 size: 'medium', 29079 padded: false, 29080 transparent: true, 29081 offset: 0 29082 }); 29083 } 29084 }; 29085 29086 if_block(node_14, ($$render) => { 29087 if (tool() === "erase" && eraser_options()) $$render(consequent_12); 29088 }); 29089 } 29090 29091 var node_15 = sibling(node_14, 2); 29092 29093 { 29094 var consequent_13 = ($$anchor) => { 29095 BrushOptions($$anchor, { 29096 colors: [], 29097 show_swatch: false, 29098 show_size: true, 29099 mode: 'eraser', 29100 get selected_size() { 29101 return selected_eraser_size(); 29102 }, 29103 29104 set selected_size($$value) { 29105 selected_eraser_size($$value); 29106 }, 29107 29108 get selected_color() { 29109 return selected_color(); 29110 }, 29111 29112 set selected_color($$value) { 29113 selected_color($$value); 29114 }, 29115 29116 get selected_opacity() { 29117 return selected_opacity(); 29118 }, 29119 29120 set selected_opacity($$value) { 29121 selected_opacity($$value); 29122 }, 29123 29124 get preview() { 29125 return preview(); 29126 }, 29127 29128 set preview($$value) { 29129 preview($$value); 29130 }, 29131 29132 $$events: { 29133 click_outside: (e) => { 29134 e.stopPropagation(); 29135 preview(false); 29136 show_eraser_size(false); 29137 handle_subtool_click(e, null); 29138 } 29139 }, 29140 $$legacy: true 29141 }); 29142 }; 29143 29144 if_block(node_15, ($$render) => { 29145 if (show_eraser_size()) $$render(consequent_13); 29146 }); 29147 } 29148 29149 reset(div_2); 29150 reset(div); 29151 29152 template_effect(() => classes = set_class(div_2, 1, 'half-container right svelte-1uf24la', null, classes, { 29153 hide: tool() === "pan" || tool() === "image" && !background() && sources().length === 0 || tool() === "image" && background() && transforms().length === 0 29154 })); 29155 29156 append($$anchor, div); 29157 29158 return pop($$exports); 29159} 29160 29161var vertex = "in vec2 aPosition;\nout vec2 vTextureCoord;\n\nuniform vec4 uInputSize;\nuniform vec4 uOutputFrame;\nuniform vec4 uOutputTexture;\n\nvec4 filterVertexPosition( void )\n{\n vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;\n \n position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;\n position.y = position.y * (2.0*uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;\n\n return vec4(position, 0.0, 1.0);\n}\n\nvec2 filterTextureCoord( void )\n{\n return aPosition * (uOutputFrame.zw * uInputSize.zw);\n}\n\nvoid main(void)\n{\n gl_Position = filterVertexPosition();\n vTextureCoord = filterTextureCoord();\n}\n"; 29162 29163var wgslVertex = "struct GlobalFilterUniforms {\n uInputSize:vec4<f32>,\n uInputPixel:vec4<f32>,\n uInputClamp:vec4<f32>,\n uOutputFrame:vec4<f32>,\n uGlobalFrame:vec4<f32>,\n uOutputTexture:vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;\n\nstruct VSOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) uv : vec2<f32>\n };\n\nfn filterVertexPosition(aPosition:vec2<f32>) -> vec4<f32>\n{\n var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;\n\n position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;\n position.y = position.y * (2.0*gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;\n\n return vec4(position, 0.0, 1.0);\n}\n\nfn filterTextureCoord( aPosition:vec2<f32> ) -> vec2<f32>\n{\n return aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);\n}\n\nfn globalTextureCoord( aPosition:vec2<f32> ) -> vec2<f32>\n{\n return (aPosition.xy / gfu.uGlobalFrame.zw) + (gfu.uGlobalFrame.xy / gfu.uGlobalFrame.zw); \n}\n\nfn getSize() -> vec2<f32>\n{\n return gfu.uGlobalFrame.zw;\n}\n \n@vertex\nfn mainVertex(\n @location(0) aPosition : vec2<f32>, \n) -> VSOutput {\n return VSOutput(\n filterVertexPosition(aPosition),\n filterTextureCoord(aPosition)\n );\n}"; 29164 29165var fragment$1 = "\nin vec2 vTextureCoord;\nout vec4 finalColor;\n\nuniform sampler2D uTexture;\nuniform vec2 uOffset;\n\nvoid main(void)\n{\n vec4 color = vec4(0.0);\n\n // Sample top left pixel\n color += texture(uTexture, vec2(vTextureCoord.x - uOffset.x, vTextureCoord.y + uOffset.y));\n\n // Sample top right pixel\n color += texture(uTexture, vec2(vTextureCoord.x + uOffset.x, vTextureCoord.y + uOffset.y));\n\n // Sample bottom right pixel\n color += texture(uTexture, vec2(vTextureCoord.x + uOffset.x, vTextureCoord.y - uOffset.y));\n\n // Sample bottom left pixel\n color += texture(uTexture, vec2(vTextureCoord.x - uOffset.x, vTextureCoord.y - uOffset.y));\n\n // Average\n color *= 0.25;\n\n finalColor = color;\n}"; 29166 29167var source$1 = "struct KawaseBlurUniforms {\n uOffset:vec2<f32>,\n};\n\n@group(0) @binding(1) var uTexture: texture_2d<f32>; \n@group(0) @binding(2) var uSampler: sampler;\n@group(1) @binding(0) var<uniform> kawaseBlurUniforms : KawaseBlurUniforms;\n\n@fragment\nfn mainFragment(\n @builtin(position) position: vec4<f32>,\n @location(0) uv : vec2<f32>\n) -> @location(0) vec4<f32> {\n let uOffset = kawaseBlurUniforms.uOffset;\n var color: vec4<f32> = vec4<f32>(0.0);\n\n // Sample top left pixel\n color += textureSample(uTexture, uSampler, vec2<f32>(uv.x - uOffset.x, uv.y + uOffset.y));\n // Sample top right pixel\n color += textureSample(uTexture, uSampler, vec2<f32>(uv.x + uOffset.x, uv.y + uOffset.y));\n // Sample bottom right pixel\n color += textureSample(uTexture, uSampler, vec2<f32>(uv.x + uOffset.x, uv.y - uOffset.y));\n // Sample bottom left pixel\n color += textureSample(uTexture, uSampler, vec2<f32>(uv.x - uOffset.x, uv.y - uOffset.y));\n // Average\n color *= 0.25;\n\n return color;\n}"; 29168 29169var fragmentClamp = "\nprecision highp float;\nin vec2 vTextureCoord;\nout vec4 finalColor;\n\nuniform sampler2D uTexture;\nuniform vec2 uOffset;\n\nuniform vec4 uInputClamp;\n\nvoid main(void)\n{\n vec4 color = vec4(0.0);\n\n // Sample top left pixel\n color += texture(uTexture, clamp(vec2(vTextureCoord.x - uOffset.x, vTextureCoord.y + uOffset.y), uInputClamp.xy, uInputClamp.zw));\n\n // Sample top right pixel\n color += texture(uTexture, clamp(vec2(vTextureCoord.x + uOffset.x, vTextureCoord.y + uOffset.y), uInputClamp.xy, uInputClamp.zw));\n\n // Sample bottom right pixel\n color += texture(uTexture, clamp(vec2(vTextureCoord.x + uOffset.x, vTextureCoord.y - uOffset.y), uInputClamp.xy, uInputClamp.zw));\n\n // Sample bottom left pixel\n color += texture(uTexture, clamp(vec2(vTextureCoord.x - uOffset.x, vTextureCoord.y - uOffset.y), uInputClamp.xy, uInputClamp.zw));\n\n // Average\n color *= 0.25;\n\n finalColor = color;\n}\n"; 29170 29171var sourceClamp = "struct KawaseBlurUniforms {\n uOffset:vec2<f32>,\n};\n\nstruct GlobalFilterUniforms {\n uInputSize:vec4<f32>,\n uInputPixel:vec4<f32>,\n uInputClamp:vec4<f32>,\n uOutputFrame:vec4<f32>,\n uGlobalFrame:vec4<f32>,\n uOutputTexture:vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;\n\n@group(0) @binding(1) var uTexture: texture_2d<f32>; \n@group(0) @binding(2) var uSampler: sampler;\n@group(1) @binding(0) var<uniform> kawaseBlurUniforms : KawaseBlurUniforms;\n\n@fragment\nfn mainFragment(\n @builtin(position) position: vec4<f32>,\n @location(0) uv : vec2<f32>\n) -> @location(0) vec4<f32> {\n let uOffset = kawaseBlurUniforms.uOffset;\n var color: vec4<f32> = vec4(0.0);\n\n // Sample top left pixel\n color += textureSample(uTexture, uSampler, clamp(vec2<f32>(uv.x - uOffset.x, uv.y + uOffset.y), gfu.uInputClamp.xy, gfu.uInputClamp.zw));\n // Sample top right pixel\n color += textureSample(uTexture, uSampler, clamp(vec2<f32>(uv.x + uOffset.x, uv.y + uOffset.y), gfu.uInputClamp.xy, gfu.uInputClamp.zw));\n // Sample bottom right pixel\n color += textureSample(uTexture, uSampler, clamp(vec2<f32>(uv.x + uOffset.x, uv.y - uOffset.y), gfu.uInputClamp.xy, gfu.uInputClamp.zw));\n // Sample bottom left pixel\n color += textureSample(uTexture, uSampler, clamp(vec2<f32>(uv.x - uOffset.x, uv.y - uOffset.y), gfu.uInputClamp.xy, gfu.uInputClamp.zw));\n // Average\n color *= 0.25;\n \n return color;\n}"; 29172 29173var __defProp$1 = Object.defineProperty; 29174var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 29175var __publicField$1 = (obj, key, value) => { 29176 __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value); 29177 return value; 29178}; 29179const _KawaseBlurFilter = class _KawaseBlurFilter extends Filter { 29180 /** @ignore */ 29181 constructor(...args) { 29182 let options = args[0] ?? {}; 29183 if (typeof options === "number" || Array.isArray(options)) { 29184 deprecation("6.0.0", "KawaseBlurFilter constructor params are now options object. See params: { strength, quality, clamp, pixelSize }"); 29185 options = { strength: options }; 29186 if (args[1] !== void 0) 29187 options.quality = args[1]; 29188 if (args[2] !== void 0) 29189 options.clamp = args[2]; 29190 } 29191 options = { ..._KawaseBlurFilter.DEFAULT_OPTIONS, ...options }; 29192 const gpuProgram = GpuProgram.from({ 29193 vertex: { 29194 source: wgslVertex, 29195 entryPoint: "mainVertex" 29196 }, 29197 fragment: { 29198 source: options?.clamp ? sourceClamp : source$1, 29199 entryPoint: "mainFragment" 29200 } 29201 }); 29202 const glProgram = GlProgram.from({ 29203 vertex, 29204 fragment: options?.clamp ? fragmentClamp : fragment$1, 29205 name: "kawase-blur-filter" 29206 }); 29207 super({ 29208 gpuProgram, 29209 glProgram, 29210 resources: { 29211 kawaseBlurUniforms: { 29212 uOffset: { value: new Float32Array(2), type: "vec2<f32>" } 29213 } 29214 } 29215 }); 29216 __publicField$1(this, "uniforms"); 29217 __publicField$1(this, "_pixelSize", { x: 0, y: 0 }); 29218 __publicField$1(this, "_clamp"); 29219 __publicField$1(this, "_kernels", []); 29220 __publicField$1(this, "_blur");
29221 __publicField$1(this, "_quality"); 29222 this.uniforms = this.resources.kawaseBlurUniforms.uniforms; 29223 this.pixelSize = options.pixelSize ?? { x: 1, y: 1 }; 29224 if (Array.isArray(options.strength)) { 29225 this.kernels = options.strength; 29226 } else if (typeof options.strength === "number") { 29227 this._blur = options.strength; 29228 this.quality = options.quality ?? 3; 29229 } 29230 this._clamp = !!options.clamp; 29231 } 29232 /** 29233 * Override existing apply method in `Filter` 29234 * @override 29235 * @ignore 29236 */ 29237 apply(filterManager, input, output, clearMode) { 29238 const uvX = this.pixelSizeX / input.source.width; 29239 const uvY = this.pixelSizeY / input.source.height; 29240 let offset; 29241 if (this._quality === 1 || this._blur === 0) { 29242 offset = this._kernels[0] + 0.5; 29243 this.uniforms.uOffset[0] = offset * uvX; 29244 this.uniforms.uOffset[1] = offset * uvY; 29245 filterManager.applyFilter(this, input, output, clearMode); 29246 } else { 29247 const renderTarget = TexturePool.getSameSizeTexture(input); 29248 let source2 = input; 29249 let target = renderTarget; 29250 let tmp; 29251 const last = this._quality - 1; 29252 for (let i = 0; i < last; i++) { 29253 offset = this._kernels[i] + 0.5; 29254 this.uniforms.uOffset[0] = offset * uvX; 29255 this.uniforms.uOffset[1] = offset * uvY; 29256 filterManager.applyFilter(this, source2, target, true); 29257 tmp = source2; 29258 source2 = target; 29259 target = tmp; 29260 } 29261 offset = this._kernels[last] + 0.5; 29262 this.uniforms.uOffset[0] = offset * uvX; 29263 this.uniforms.uOffset[1] = offset * uvY; 29264 filterManager.applyFilter(this, source2, output, clearMode); 29265 TexturePool.returnTexture(renderTarget); 29266 } 29267 } 29268 /** 29269 * The amount of blur, value greater than `0`. 29270 * @default 4 29271 */ 29272 get strength() { 29273 return this._blur; 29274 } 29275 set strength(value) { 29276 this._blur = value; 29277 this._generateKernels(); 29278 } 29279 /** 29280 * The quality of the filter, integer greater than `1`. 29281 * @default 3 29282 */ 29283 get quality() { 29284 return this._quality; 29285 } 29286 set quality(value) { 29287 this._quality = Math.max(1, Math.round(value)); 29288 this._generateKernels(); 29289 } 29290 /** 29291 * The kernel size of the blur filter, for advanced usage 29292 * @default [0] 29293 */ 29294 get kernels() { 29295 return this._kernels; 29296 } 29297 set kernels(value) { 29298 if (Array.isArray(value) && value.length > 0) { 29299 this._kernels = value; 29300 this._quality = value.length; 29301 this._blur = Math.max(...value); 29302 } else { 29303 this._kernels = [0]; 29304 this._quality = 1; 29305 } 29306 } 29307 /** 29308 * The size of the pixels. Large size is blurrier. For advanced usage. 29309 * @default {x:1,y:1} 29310 */ 29311 get pixelSize() { 29312 return this._pixelSize; 29313 } 29314 set pixelSize(value) { 29315 if (typeof value === "number") { 29316 this.pixelSizeX = this.pixelSizeY = value; 29317 return; 29318 } 29319 if (Array.isArray(value)) { 29320 this.pixelSizeX = value[0]; 29321 this.pixelSizeY = value[1]; 29322 return; 29323 } 29324 this._pixelSize = value; 29325 } 29326 /** 29327 * The size of the pixels on the `x` axis. Large size is blurrier. For advanced usage. 29328 * @default 1 29329 */ 29330 get pixelSizeX() { 29331 return this.pixelSize.x; 29332 } 29333 set pixelSizeX(value) { 29334 this.pixelSize.x = value; 29335 } 29336 /** 29337 * The size of the pixels on the `y` axis. Large size is blurrier. For advanced usage. 29338 * @default 1 29339 */ 29340 get pixelSizeY() { 29341 return this.pixelSize.y; 29342 } 29343 set pixelSizeY(value) { 29344 this.pixelSize.y = value; 29345 } 29346 /** 29347 * Get the if the filter is clamped 29348 * @default false 29349 */ 29350 get clamp() { 29351 return this._clamp; 29352 } 29353 /** Update padding based on kernel data */ 29354 _updatePadding() { 29355 this.padding = Math.ceil(this._kernels.reduce((acc, v) => acc + v + 0.5, 0)); 29356 } 29357 /** Auto generate kernels by blur & quality */ 29358 _generateKernels() { 29359 const blur = this._blur; 29360 const quality = this._quality; 29361 const kernels = [blur]; 29362 if (blur > 0) { 29363 let k = blur; 29364 const step = blur / quality; 29365 for (let i = 1; i < quality; i++) { 29366 k -= step; 29367 kernels.push(k); 29368 } 29369 } 29370 this._kernels = kernels; 29371 this._updatePadding(); 29372 } 29373}; 29374/** Default values for options. */ 29375__publicField$1(_KawaseBlurFilter, "DEFAULT_OPTIONS", { 29376 strength: 4,
29377 quality: 3, 29378 clamp: false, 29379 pixelSize: { x: 1, y: 1 } 29380}); 29381let KawaseBlurFilter = _KawaseBlurFilter; 29382 29383var fragment = "precision highp float;\nin vec2 vTextureCoord;\nout vec4 finalColor;\n\nuniform sampler2D uTexture;\nuniform float uAlpha;\nuniform vec3 uColor;\nuniform vec2 uOffset;\n\nuniform vec4 uInputSize;\n\nvoid main(void){\n vec4 sample = texture(uTexture, vTextureCoord - uOffset * uInputSize.zw);\n\n // Premultiply alpha\n sample.rgb = uColor.rgb * sample.a;\n\n // alpha user alpha\n sample *= uAlpha;\n\n finalColor = sample;\n}"; 29384 29385var source = "struct DropShadowUniforms {\n uAlpha: f32,\n uColor: vec3<f32>,\n uOffset: vec2<f32>,\n};\n\nstruct GlobalFilterUniforms {\n uInputSize:vec4<f32>,\n uInputPixel:vec4<f32>,\n uInputClamp:vec4<f32>,\n uOutputFrame:vec4<f32>,\n uGlobalFrame:vec4<f32>,\n uOutputTexture:vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;\n\n@group(0) @binding(1) var uTexture: texture_2d<f32>; \n@group(0) @binding(2) var uSampler: sampler;\n@group(1) @binding(0) var<uniform> dropShadowUniforms : DropShadowUniforms;\n\n@fragment\nfn mainFragment(\n @builtin(position) position: vec4<f32>,\n @location(0) uv : vec2<f32>\n) -> @location(0) vec4<f32> {\n var color: vec4<f32> = textureSample(uTexture, uSampler, uv - dropShadowUniforms.uOffset * gfu.uInputSize.zw);\n\n // Premultiply alpha\n color = vec4<f32>(vec3<f32>(dropShadowUniforms.uColor.rgb * color.a), color.a);\n // alpha user alpha\n color *= dropShadowUniforms.uAlpha;\n\n return color;\n}"; 29386 29387var __defProp = Object.defineProperty; 29388var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 29389var __publicField = (obj, key, value) => { 29390 __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); 29391 return value; 29392}; 29393const _DropShadowFilter = class _DropShadowFilter extends Filter { 29394 /** 29395 * @param options - Options for the DropShadowFilter constructor. 29396 */ 29397 constructor(options) { 29398 options = { ..._DropShadowFilter.DEFAULT_OPTIONS, ...options }; 29399 const gpuProgram = GpuProgram.from({ 29400 vertex: { 29401 source: wgslVertex, 29402 entryPoint: "mainVertex" 29403 }, 29404 fragment: { 29405 source, 29406 entryPoint: "mainFragment" 29407 } 29408 }); 29409 const glProgram = GlProgram.from({ 29410 vertex, 29411 fragment, 29412 name: "drop-shadow-filter" 29413 }); 29414 super({ 29415 gpuProgram, 29416 glProgram, 29417 resources: { 29418 dropShadowUniforms: { 29419 uAlpha: { value: options.alpha, type: "f32" }, 29420 uColor: { value: new Float32Array(3), type: "vec3<f32>" }, 29421 uOffset: { value: options.offset, type: "vec2<f32>" } 29422 } 29423 }, 29424 resolution: options.resolution 29425 }); 29426 __publicField(this, "uniforms"); 29427 /** 29428 * Hide the contents, only show the shadow. 29429 * @default false 29430 */ 29431 __publicField(this, "shadowOnly", false); 29432 __publicField(this, "_color"); 29433 __publicField(this, "_blurFilter"); 29434 __publicField(this, "_basePass"); 29435 this.uniforms = this.resources.dropShadowUniforms.uniforms; 29436 this._color = new Color(); 29437 this.color = options.color ?? 0; 29438 this._blurFilter = new KawaseBlurFilter({ 29439 strength: options.kernels ?? options.blur, 29440 quality: options.kernels ? void 0 : options.quality 29441 }); 29442 this._basePass = new Filter({ 29443 gpuProgram: GpuProgram.from({ 29444 vertex: { 29445 source: wgslVertex, 29446 entryPoint: "mainVertex" 29447 }, 29448 fragment: { 29449 source: ` 29450 @group(0) @binding(1) var uTexture: texture_2d<f32>; 29451 @group(0) @binding(2) var uSampler: sampler; 29452 @fragment 29453 fn mainFragment( 29454 @builtin(position) position: vec4<f32>, 29455 @location(0) uv : vec2<f32> 29456 ) -> @location(0) vec4<f32> { 29457 return textureSample(uTexture, uSampler, uv); 29458 } 29459 `, 29460 entryPoint: "mainFragment" 29461 } 29462 }), 29463 glProgram: GlProgram.from({ 29464 vertex, 29465 fragment: ` 29466 in vec2 vTextureCoord; 29467 out vec4 finalColor; 29468 uniform sampler2D uTexture; 29469 29470 void main(void){ 29471 finalColor = texture(uTexture, vTextureCoord); 29472 } 29473 `, 29474 name: "drop-shadow-filter" 29475 }), 29476 resources: {} 29477 });
29478 Object.assign(this, options); 29479 } 29480 /** 29481 * Override existing apply method in `Filter` 29482 * @override 29483 * @ignore 29484 */ 29485 apply(filterManager, input, output, clearMode) { 29486 const renderTarget = TexturePool.getSameSizeTexture(input); 29487 filterManager.applyFilter(this, input, renderTarget, true); 29488 this._blurFilter.apply(filterManager, renderTarget, output, clearMode); 29489 if (!this.shadowOnly) { 29490 filterManager.applyFilter(this._basePass, input, output, false); 29491 } 29492 TexturePool.returnTexture(renderTarget); 29493 } 29494 /** 29495 * Set the offset position of the drop-shadow relative to the original image. 29496 * @default [4,4] 29497 */ 29498 get offset() { 29499 return this.uniforms.uOffset; 29500 } 29501 set offset(value) { 29502 this.uniforms.uOffset = value; 29503 this._updatePadding(); 29504 } 29505 /** 29506 * Set the offset position of the drop-shadow relative to the original image on the `x` axis 29507 * @default 4 29508 */ 29509 get offsetX() { 29510 return this.offset.x; 29511 } 29512 set offsetX(value) { 29513 this.offset.x = value; 29514 this._updatePadding(); 29515 } 29516 /** 29517 * Set the offset position of the drop-shadow relative to the original image on the `y` axis 29518 * @default 4 29519 */ 29520 get offsetY() { 29521 return this.offset.y; 29522 } 29523 set offsetY(value) { 29524 this.offset.y = value; 29525 this._updatePadding(); 29526 } 29527 /** 29528 * The color value of shadow. 29529 * @example [0.0, 0.0, 0.0] = 0x000000 29530 * @default 0x000000 29531 */ 29532 get color() { 29533 return this._color.value; 29534 } 29535 set color(value) { 29536 this._color.setValue(value); 29537 const [r, g, b] = this._color.toArray(); 29538 this.uniforms.uColor[0] = r; 29539 this.uniforms.uColor[1] = g; 29540 this.uniforms.uColor[2] = b; 29541 } 29542 /** 29543 * Coefficient for alpha multiplication 29544 * @default 1 29545 */ 29546 get alpha() { 29547 return this.uniforms.uAlpha; 29548 } 29549 set alpha(value) { 29550 this.uniforms.uAlpha = value; 29551 } 29552 /** 29553 * The strength of the shadow's blur. 29554 * @default 2 29555 */ 29556 get blur() { 29557 return this._blurFilter.strength; 29558 } 29559 set blur(value) { 29560 this._blurFilter.strength = value; 29561 this._updatePadding(); 29562 } 29563 /** 29564 * Sets the quality of the Blur Filter 29565 * @default 4 29566 */ 29567 get quality() { 29568 return this._blurFilter.quality; 29569 } 29570 set quality(value) { 29571 this._blurFilter.quality = value; 29572 this._updatePadding(); 29573 } 29574 /** Sets the kernels of the Blur Filter */ 29575 get kernels() { 29576 return this._blurFilter.kernels; 29577 } 29578 set kernels(value) { 29579 this._blurFilter.kernels = value; 29580 } 29581 /** 29582 * Sets the pixelSize of the Kawase Blur filter 29583 * @default [1,1] 29584 */ 29585 get pixelSize() { 29586 return this._blurFilter.pixelSize; 29587 } 29588 set pixelSize(value) { 29589 if (typeof value === "number") { 29590 value = { x: value, y: value }; 29591 } 29592 if (Array.isArray(value)) { 29593 value = { x: value[0], y: value[1] }; 29594 } 29595 this._blurFilter.pixelSize = value; 29596 } 29597 /** 29598 * Sets the pixelSize of the Kawase Blur filter on the `x` axis 29599 * @default 1 29600 */ 29601 get pixelSizeX() { 29602 return this._blurFilter.pixelSizeX; 29603 } 29604 set pixelSizeX(value) { 29605 this._blurFilter.pixelSizeX = value; 29606 } 29607 /** 29608 * Sets the pixelSize of the Kawase Blur filter on the `y` axis 29609 * @default 1 29610 */ 29611 get pixelSizeY() { 29612 return this._blurFilter.pixelSizeY; 29613 } 29614 set pixelSizeY(value) { 29615 this._blurFilter.pixelSizeY = value; 29616 } 29617 /** 29618 * Recalculate the proper padding amount. 29619 * @private 29620 */ 29621 _updatePadding() { 29622 const offsetPadding = Math.max( 29623 Math.abs(this.offsetX), 29624 Math.abs(this.offsetY) 29625 ); 29626 this.padding = offsetPadding + this.blur * 2 + this.quality * 4; 29627 } 29628}; 29629/** Default values for options. */ 29630__publicField(_DropShadowFilter, "DEFAULT_OPTIONS", { 29631 offset: { x: 4, y: 4 }, 29632 color: 0, 29633 alpha: 0.5, 29634 shadowOnly: false, 29635 kernels: void 0, 29636 blur: 2, 29637 quality: 3, 29638 pixelSize: { x: 1, y: 1 }, 29639 resolution: 1 29640}); 29641let DropShadowFilter = _DropShadowFilter; 29642 29643class ImageTool { 29644 name = "image"; 29645 context; 29646 current_tool; 29647 current_subtool; 29648 async setup(context, tool, subtool) { 29649 this.context = context; 29650 this.current_tool = tool; 29651 this.current_subtool = subtool; 29652 } 29653 cleanup() { 29654 } 29655 async add_image({ 29656 image, 29657 fixed_canvas, 29658 border_region = 0 29659 }) { 29660 const image_command = new AddImageCommand( 29661 this.context, 29662 image, 29663 fixed_canvas, 29664 border_region 29665 ); 29666 await this.context.execute_command(image_command); 29667 } 29668 set_tool(tool, subtool) { 29669 this.current_tool = tool; 29670 this.current_subtool = subtool; 29671 } 29672} 29673class AddImageCommand { 29674 sprite = null; 29675 fixed_canvas; 29676 context; 29677 background; 29678 current_canvas_size; 29679 computed_dimensions; 29680 current_scale; 29681 current_position; 29682 border_region; 29683 scaled_width; 29684 scaled_height; 29685 previous_image = null; 29686 name; 29687 constructor(context, background, fixed_canvas, border_region = 0) { 29688 this.name = "AddImage"; 29689 this.context = context; 29690 this.background = background; 29691 this.fixed_canvas = fixed_canvas; 29692 this.border_region = border_region; 29693 this.current_canvas_size = get$1(this.context.dimensions); 29694 this.current_scale = get$1(this.context.scale); 29695 this.current_position = get$1(this.context.position); 29696 this.computed_dimensions = { width: 0, height: 0 }; 29697 if (this.context.background_image && this.context.background_image.texture) { 29698 const bg = this.context.background_image; 29699 const stored_border_region = bg.borderRegion || 0; 29700 this.previous_image = { 29701 texture: this.clone_texture(bg.texture), 29702 width: bg.width, 29703 height: bg.height, 29704 x: bg.position.x, 29705 y: bg.position.y, 29706 border_region: stored_border_region 29707 }; 29708 } 29709 } 29710 /** 29711 * Creates a copy of a texture to avoid reference issues 29712 */ 29713 clone_texture(texture) { 29714 const render_texture = RenderTexture.create({ 29715 width: texture.width, 29716 height: texture.height, 29717 resolution: window.devicePixelRatio || 1 29718 }); 29719 const sprite = new Sprite(texture); 29720 const container = new Container(); 29721 container.addChild(sprite); 29722 this.context.app.renderer.render(container, { 29723 renderTexture: render_texture 29724 }); 29725 container.destroy({ children: true }); 29726 return render_texture; 29727 } 29728 async start() { 29729 let image_texture; 29730 if (this.background instanceof Texture) { 29731 image_texture = this.background; 29732 } else { 29733 const img = await createImageBitmap(this.background); 29734 image_texture = Texture.from(img); 29735 } 29736 this.sprite = new Sprite(image_texture); 29737 const [width, height] = this.handle_image(); 29738 this.computed_dimensions = { width, height }; 29739 } 29740 handle_image() { 29741 if (this.sprite === null) { 29742 return [0, 0]; 29743 } 29744 if (this.fixed_canvas) { 29745 const effectiveCanvasWidth = Math.max( 29746 this.current_canvas_size.width - this.border_region * 2, 29747 10 29748 ); 29749 const effectiveCanvasHeight = Math.max( 29750 this.current_canvas_size.height - this.border_region * 2, 29751 10 29752 ); 29753 const { width, height, x, y } = fit_image_to_canvas( 29754 this.sprite.width, 29755 this.sprite.height, 29756 effectiveCanvasWidth, 29757 effectiveCanvasHeight 29758 ); 29759 this.sprite.width = width; 29760 this.sprite.height = height; 29761 this.sprite.x = x + this.border_region; 29762 this.sprite.y = y + this.border_region; 29763 } else { 29764 const width = this.sprite.width; 29765 const height = this.sprite.height; 29766 this.sprite.x = this.border_region; 29767 this.sprite.y = this.border_region; 29768 return [width + this.border_region * 2, height + this.border_region * 2]; 29769 } 29770 return [this.current_canvas_size.width, this.current_canvas_size.height]; 29771 } 29772 async execute(context) { 29773 if (context) { 29774 this.context = context; 29775 } 29776 await this.start(); 29777 if (this.sprite === null) { 29778 return; 29779 } 29780 const { width, height } = this.computed_dimensions; 29781 await this.context.set_image_properties({ 29782 scale: 1, 29783 position: { 29784 x: this.context.app.screen.width / 2, 29785 y: this.context.app.screen.height / 2 29786 }, 29787 width: this.fixed_canvas ? this.current_canvas_size.width : width, 29788 height: this.fixed_canvas ? this.current_canvas_size.height : height 29789 }); 29790 const background_layer = this.context.layer_manager.create_background_layer( 29791 this.fixed_canvas ? this.current_canvas_size.width : width, 29792 this.fixed_canvas ? this.current_canvas_size.height : height 29793 ); 29794 this.sprite.zIndex = 0; 29795 background_layer.addChild(this.sprite); 29796 this.context.layer_manager.reset_layers( 29797 this.fixed_canvas ? this.current_canvas_size.width : width, 29798 this.fixed_canvas ? this.current_canvas_size.height : height, 29799 true 29800 ); 29801 if (this.border_region > 0) { 29802 this.sprite.borderRegion = this.border_region; 29803 } 29804 this.context.set_background_image(this.sprite); 29805 this.context.reset(); 29806 } 29807 async undo() { 29808 if (this.sprite) { 29809 this.sprite.destroy(); 29810 if (this.previous_image) { 29811 const previous_sprite = new Sprite(this.previous_image.texture); 29812 previous_sprite.width = this.previous_image.width; 29813 previous_sprite.height = this.previous_image.height; 29814 previous_sprite.position.set( 29815 this.previous_image.x, 29816 this.previous_image.y 29817 ); 29818 if (this.previous_image.border_region > 0) { 29819 previous_sprite.borderRegion = this.previous_image.border_region; 29820 } 29821 await this.context.set_image_properties({ 29822 scale: 1, 29823 position: { 29824 x: this.context.app.screen.width / 2, 29825 y: this.context.app.screen.height / 2 29826 }, 29827 width: this.previous_image.width, 29828 height: this.previous_image.height 29829 }); 29830 const background_layer = this.context.layer_manager.create_background_layer( 29831 this.previous_image.width, 29832 this.previous_image.height 29833 ); 29834 previous_sprite.zIndex = 0; 29835 background_layer.addChild(previous_sprite); 29836 this.context.layer_manager.reset_layers( 29837 this.previous_image.width, 29838 this.previous_image.height, 29839 true 29840 ); 29841 this.context.set_background_image(previous_sprite); 29842 } else { 29843 await this.context.set_image_properties({ 29844 scale: 1, 29845 position: { 29846 x: this.context.app.screen.width / 2, 29847 y: this.context.app.screen.height / 2 29848 }, 29849 width: this.current_canvas_size.width, 29850 height: this.current_canvas_size.height 29851 }); 29852 this.context.layer_manager.create_background_layer( 29853 this.current_canvas_size.width, 29854 this.current_canvas_size.height 29855 ); 29856 this.context.layer_manager.reset_layers( 29857 this.current_canvas_size.width, 29858 this.current_canvas_size.height 29859 ); 29860 } 29861 this.context.reset(); 29862 } 29863 } 29864} 29865function fit_image_to_canvas(image_width, image_height, canvas_width, canvas_height) { 29866 const image_aspect_ratio = image_width / image_height; 29867 const canvas_aspect_ratio = canvas_width / canvas_height; 29868 let new_width; 29869 let new_height; 29870 if (image_aspect_ratio > canvas_aspect_ratio) { 29871 new_width = canvas_width; 29872 new_height = canvas_width / image_aspect_ratio; 29873 } else { 29874 new_height = canvas_height; 29875 new_width = canvas_height * image_aspect_ratio; 29876 } 29877 const x = Math.round((canvas_width - new_width) / 2); 29878 const y = Math.round((canvas_height - new_height) / 2); 29879 return { 29880 width: Math.round(new_width), 29881 height: Math.round(new_height), 29882 x, 29883 y 29884 }; 29885} 29886 29887class ZoomTool { 29888 name = "zoom"; 29889 min_zoom = writable(true); 29890 image_editor_context; 29891 max_zoom = 10; 29892 border_padding = 30; 29893 pad_bottom = 0; 29894 is_pinching = false;
29895 is_dragging = false; 29896 is_pointer_dragging = false; 29897 last_touch_position = null; 29898 last_pinch_distance = 0; 29899 drag_start = new Point(); 29900 current_tool; 29901 current_subtool; 29902 local_scale = 1; 29903 local_dimensions = { 29904 width: 0, 29905 height: 0 29906 }; 29907 local_position = { x: 0, y: 0 }; 29908 /** 29909 * Prevents default behavior for events 29910 * @param {FederatedWheelEvent | FederatedPointerEvent} event - The event to prevent default behavior for 29911 */ 29912 prevent_default(event) { 29913 event.preventDefault(); 29914 event.stopPropagation(); 29915 } 29916 /** 29917 * Sets the current tool and subtool 29918 * @param {ToolbarTool} tool - The tool to set 29919 * @param {Subtool} subtool - The subtool to set 29920 */ 29921 set_tool(tool, subtool) { 29922 this.current_tool = tool; 29923 this.current_subtool = subtool; 29924 } 29925 /** 29926 * Sets the zoom level to a specific percentage or fits the image to the screen 29927 * @param {number | 'fit'} zoom_level - The zoom level to set (0-1) or 'fit' to use min_zoom 29928 * @returns {void} 29929 */ 29930 set_zoom(zoom_level) { 29931 const fit_zoom = this.calculate_min_zoom( 29932 this.local_dimensions.width, 29933 this.local_dimensions.height 29934 ); 29935 let target_zoom; 29936 const is_fit_zoom = zoom_level === "fit"; 29937 if (is_fit_zoom) { 29938 target_zoom = fit_zoom; 29939 } else { 29940 target_zoom = Math.max(0, Math.min(this.max_zoom, zoom_level)); 29941 } 29942 const canvas = this.image_editor_context.app.screen; 29943 const canvas_width = canvas.width; 29944 const canvas_height = canvas.height; 29945 let center_point; 29946 center_point = { 29947 x: canvas_width / 2, 29948 y: canvas_height / 2 29949 }; 29950 this.zoom_to_point(target_zoom, center_point, true, is_fit_zoom); 29951 } 29952 /** 29953 * Sets up the zoom tool with the given context and state 29954 * @param {ImageEditorContext} context - The image editor context 29955 * @param {ToolbarTool} tool - The current tool 29956 * @param {Subtool} subtool - The current subtool 29957 * @returns {Promise<void>} 29958 */ 29959 async setup(context, tool, subtool) { 29960 this.image_editor_context = context; 29961 this.current_tool = tool; 29962 this.current_subtool = subtool; 29963 this.pad_bottom = context.pad_bottom; 29964 const { width, height } = await this.get_container_dimensions(); 29965 const fit_zoom = this.calculate_min_zoom(width, height); 29966 const min_zoom = Math.min(fit_zoom, 1); 29967 this.local_scale = min_zoom; 29968 const canvas = this.image_editor_context.app.screen; 29969 const center_x = canvas.width / 2; 29970 const center_y = canvas.height / 2; 29971 const scaled_width = width * min_zoom; 29972 const scaled_height = height * min_zoom; 29973 const initial_x = center_x - scaled_width / 2; 29974 const initial_y = center_y - scaled_height / 2; 29975 this.local_position = { x: initial_x, y: initial_y }; 29976 this.setup_event_listeners(); 29977 await this.image_editor_context.set_image_properties({ 29978 scale: min_zoom, 29979 position: { x: initial_x, y: initial_y } 29980 }); 29981 this.image_editor_context.dimensions.subscribe((dimensions) => { 29982 this.local_dimensions = dimensions; 29983 }); 29984 this.image_editor_context.scale.subscribe((scale) => { 29985 this.local_scale = scale; 29986 }); 29987 this.image_editor_context.position.subscribe((position) => { 29988 this.local_position = position; 29989 }); 29990 } 29991 /** 29992 * Sets up event listeners for zoom and pan functionality 29993 * @private 29994 */ 29995 setup_event_listeners() { 29996 const stage = this.image_editor_context.app.stage; 29997 const canvas = this.image_editor_context.app.canvas; 29998 canvas.addEventListener("wheel", this.prevent_default, { passive: false }); 29999 stage.eventMode = "static"; 30000 stage.hitArea = this.image_editor_context.app.screen; 30001 const wheel_listener = this.handle_wheel.bind(this); 30002 stage.addEventListener("wheel", wheel_listener, { passive: false }); 30003 if ("ontouchstart" in window) { 30004 stage.addEventListener( 30005 "touchstart", 30006 this.handle_touch_start.bind(this) 30007 ); 30008 stage.addEventListener( 30009 "touchmove", 30010 this.handle_touch_move.bind(this) 30011 ); 30012 stage.addEventListener( 30013 "touchend", 30014 this.handle_touch_end.bind(this) 30015 ); 30016 } else { 30017 stage.addEventListener( 30018 "pointerdown", 30019 this.handle_pointer_down.bind(this) 30020 ); 30021 stage.addEventListener( 30022 "pointermove", 30023 this.handle_pointer_move.bind(this) 30024 ); 30025 stage.addEventListener("pointerup", this.handle_pointer_up.bind(this)); 30026 stage.addEventListener( 30027 "pointerupoutside", 30028 this.handle_pointer_up.bind(this) 30029 ); 30030 } 30031 } 30032 /** 30033 * Handles wheel events for zooming 30034 * @param {FederatedWheelEvent} event - The wheel event 30035 * @private 30036 */ 30037 handle_wheel(event) { 30038 const is_trackpad = event.deltaMode === 0 && Math.abs(event.deltaY) < 50; 30039 const scroll_speed = is_trackpad ? 30 : 10; 30040 if (event.altKey || event.metaKey) { 30041 const local_point = this.image_editor_context.app.stage.toLocal( 30042 event.global 30043 ); 30044 const zoom_delta = -event.deltaY * (is_trackpad ? 1e-3 : 5e-4); 30045 const new_scale = this.local_scale * (1 + zoom_delta); 30046 this.zoom_to_point(new_scale, local_point, true); 30047 } else { 30048 const delta_x = event.deltaX; 30049 const delta_y = event.deltaY; 30050 const raw_position = { 30051 x: this.local_position.x - delta_x / 100 * scroll_speed, 30052 y: this.local_position.y - delta_y / 100 * scroll_speed 30053 }; 30054 const canvas = this.image_editor_context.app.screen; 30055 const scaled_width = this.local_dimensions.width * this.local_scale; 30056 const scaled_height = this.local_dimensions.height * this.local_scale; 30057 const available_width = canvas.width - this.border_padding * 2; 30058 const available_height = canvas.height - this.border_padding * 2 - this.pad_bottom; 30059 let final_position = { ...raw_position }; 30060 if (scaled_width <= available_width) { 30061 final_position.x = (canvas.width - scaled_width) / 2; 30062 } else { 30063 const max_offset = canvas.width / 2; 30064 const left_bound = max_offset; 30065 const right_bound = canvas.width - max_offset - scaled_width; 30066 final_position.x = Math.min( 30067 Math.max(raw_position.x, right_bound), 30068 left_bound 30069 ); 30070 } 30071 if (scaled_height <= available_height) { 30072 final_position.y = (canvas.height - this.pad_bottom - scaled_height) / 2; 30073 } else { 30074 const max_offset = (canvas.height - this.pad_bottom) / 2; 30075 const top_bound = max_offset; 30076 const bottom_bound = canvas.height - this.pad_bottom - max_offset - scaled_height; 30077 final_position.y = Math.min( 30078 Math.max(raw_position.y, bottom_bound), 30079 top_bound 30080 ); 30081 } 30082 this.image_editor_context.set_image_properties({ 30083 scale: this.local_scale, 30084 position: final_position 30085 }); 30086 } 30087 } 30088 /** 30089 * Cleans up resources and event listeners 30090 */ 30091 cleanup() { 30092 const stage = this?.image_editor_context?.app?.stage; 30093 if (!stage) return; 30094 stage.removeEventListener("wheel", this.handle_wheel.bind(this)); 30095 if ("ontouchstart" in window) { 30096 stage.removeEventListener( 30097 "touchstart", 30098 this.handle_touch_start.bind(this) 30099 ); 30100 stage.removeEventListener( 30101 "touchmove", 30102 this.handle_touch_move.bind(this) 30103 ); 30104 stage.removeEventListener( 30105 "touchend", 30106 this.handle_touch_end.bind(this) 30107 ); 30108 } else { 30109 stage.removeEventListener( 30110 "pointerdown", 30111 this.handle_pointer_down.bind(this) 30112 ); 30113 stage.removeEventListener( 30114 "pointermove", 30115 this.handle_pointer_move.bind(this) 30116 ); 30117 stage.removeEventListener("pointerup", this.handle_pointer_up.bind(this)); 30118 stage.removeEventListener( 30119 "pointerupoutside", 30120 this.handle_pointer_up.bind(this) 30121 ); 30122 } 30123 } 30124 /** 30125 * Gets the dimensions of the container 30126 * @returns {Promise<{width: number, height: number}>} The container dimensions 30127 * @private 30128 */ 30129 async get_container_dimensions() {
30130 const bounds = this.image_editor_context.image_container.getLocalBounds(); 30131 return { 30132 width: bounds.width, 30133 height: bounds.height 30134 }; 30135 } 30136 /** 30137 * Calculates the minimum zoom level to fit the image in the viewport 30138 * @param {number} container_width - The width of the container 30139 * @param {number} container_height - The height of the container 30140 * @returns {number} The minimum zoom level 30141 * @private 30142 */ 30143 calculate_min_zoom(container_width, container_height) { 30144 const canvas = this.image_editor_context.app.screen; 30145 const viewport_width = canvas.width; 30146 const viewport_height = canvas.height; 30147 if (!container_width || !viewport_width || !container_height || !viewport_height) { 30148 return 1; 30149 } 30150 const available_width = viewport_width - this.border_padding * 2; 30151 const available_height = viewport_height - this.border_padding * 2 - this.pad_bottom; 30152 const width_ratio = available_width / container_width; 30153 const height_ratio = available_height / container_height; 30154 return Math.min(width_ratio, height_ratio); 30155 } 30156 /** 30157 * Handles touch start events 30158 * @param {TouchEvent} event - The touch event 30159 * @private 30160 */ 30161 handle_touch_start(event) { 30162 event.preventDefault(); 30163 const touchEvent = event; 30164 if (touchEvent.touches && touchEvent.touches.length === 2) { 30165 this.is_pinching = true; 30166 this.last_pinch_distance = Math.hypot( 30167 touchEvent.touches[0].pageX - touchEvent.touches[1].pageX, 30168 touchEvent.touches[0].pageY - touchEvent.touches[1].pageY 30169 ); 30170 } else if (touchEvent.touches && touchEvent.touches.length === 1) { 30171 this.is_dragging = true; 30172 const rect = this.image_editor_context.app.view.getBoundingClientRect(); 30173 this.last_touch_position = new Point( 30174 touchEvent.touches[0].pageX - rect.left, 30175 touchEvent.touches[0].pageY - rect.top 30176 ); 30177 } 30178 } 30179 /** 30180 * Handles touch move events 30181 * @param {TouchEvent} event - The touch event 30182 * @private 30183 */ 30184 handle_touch_move(event) { 30185 event.preventDefault(); 30186 const touchEvent = event; 30187 if (this.is_pinching && touchEvent.touches && touchEvent.touches.length === 2) { 30188 const rect = this.image_editor_context.app.view.getBoundingClientRect(); 30189 const current_distance = Math.hypot( 30190 touchEvent.touches[0].pageX - touchEvent.touches[1].pageX, 30191 touchEvent.touches[0].pageY - touchEvent.touches[1].pageY 30192 ); 30193 const pinch_center = { 30194 x: (touchEvent.touches[0].pageX + touchEvent.touches[1].pageX) / 2 - rect.left, 30195 y: (touchEvent.touches[0].pageY + touchEvent.touches[1].pageY) / 2 - rect.top 30196 }; 30197 const scale_val = current_distance / this.last_pinch_distance; 30198 this.last_pinch_distance = current_distance; 30199 this.zoom_to_point(this.local_scale * scale_val, pinch_center); 30200 } else if (this.is_dragging && touchEvent.touches && touchEvent.touches.length === 1 && this.last_touch_position) { 30201 const rect = this.image_editor_context.app.view.getBoundingClientRect(); 30202 const current_position = new Point( 30203 touchEvent.touches[0].pageX - rect.left, 30204 touchEvent.touches[0].pageY - rect.top 30205 ); 30206 const dx = current_position.x - this.last_touch_position.x; 30207 const dy = current_position.y - this.last_touch_position.y; 30208 this.image_editor_context.set_image_properties({ 30209 position: { 30210 x: this.local_position.x + dx, 30211 y: this.local_position.y + dy 30212 } 30213 }); 30214 this.last_touch_position = current_position; 30215 } 30216 } 30217 /** 30218 * Handles touch end events 30219 * @param {TouchEvent} event - The touch event 30220 * @private 30221 */ 30222 handle_touch_end(event) { 30223 event.preventDefault(); 30224 const touchEvent = event; 30225 if (touchEvent.touches && touchEvent.touches.length < 2) { 30226 this.is_pinching = false; 30227 if (touchEvent.touches && touchEvent.touches.length === 1) { 30228 const rect = this.image_editor_context.app.view.getBoundingClientRect(); 30229 this.last_touch_position = new Point( 30230 touchEvent.touches[0].pageX - rect.left, 30231 touchEvent.touches[0].pageY - rect.top 30232 ); 30233 this.is_dragging = true; 30234 } 30235 } 30236 if (touchEvent.touches && touchEvent.touches.length === 0) { 30237 this.is_dragging = false;
30238 this.last_touch_position = null; 30239 this.last_pinch_distance = 0; 30240 } 30241 } 30242 /** 30243 * Gets the bounded position to keep the image within the viewport 30244 * @param {Object} position - The position to bound 30245 * @param {number} position.x - The x coordinate 30246 * @param {number} position.y - The y coordinate 30247 * @returns {Object} The bounded position 30248 * @private 30249 */ 30250 get_bounded_position(position) { 30251 const canvas = this.image_editor_context.app.screen; 30252 const scaled_width = this.local_dimensions.width * this.local_scale; 30253 const scaled_height = this.local_dimensions.height * this.local_scale; 30254 const center_position = { 30255 x: (canvas.width - scaled_width) / 2, 30256 y: (canvas.height - scaled_height - this.pad_bottom) / 2 30257 }; 30258 if (scaled_width <= canvas.width && scaled_height <= canvas.height) { 30259 return center_position; 30260 } 30261 let x = position.x; 30262 let y = position.y; 30263 if (scaled_width <= canvas.width) { 30264 x = center_position.x; 30265 } else { 30266 const min_x = canvas.width - scaled_width; 30267 const max_x = 0; 30268 x = Math.max(min_x, Math.min(max_x, position.x)); 30269 } 30270 if (scaled_height <= canvas.height - this.pad_bottom) { 30271 y = center_position.y; 30272 } else { 30273 const min_y = canvas.height - scaled_height - this.pad_bottom; 30274 const max_y = 0; 30275 y = Math.max(min_y, Math.min(max_y, position.y)); 30276 } 30277 return { x, y }; 30278 } 30279 /** 30280 * Zooms to a specific point with a new zoom level 30281 * @param {number} new_zoom - The new zoom level 30282 * @param {Object} point - The point to zoom to 30283 * @param {number} point.x - The x coordinate of the point 30284 * @param {number} point.y - The y coordinate of the point 30285 * @param {boolean} hard - Whether to apply a hard zoom (no animation) 30286 * @param {boolean} is_fit_zoom - Whether this is a fit zoom operation 30287 * @private 30288 */ 30289 zoom_to_point(new_zoom, point, hard, is_fit_zoom) { 30290 const container = this.image_editor_context.image_container; 30291 const current_world_pos = container.getGlobalPosition(); 30292 const current_scale = container.scale.x; 30293 const cursor_image_pixel = { 30294 x: (point.x - current_world_pos.x) / current_scale, 30295 y: (point.y - current_world_pos.y) / current_scale 30296 }; 30297 const fit_zoom = this.calculate_min_zoom( 30298 this.local_dimensions.width, 30299 this.local_dimensions.height 30300 ); 30301 const min_zoom = Math.min(fit_zoom, 1); 30302 new_zoom = Math.min(Math.max(new_zoom, min_zoom), this.max_zoom); 30303 const new_container_world_pos = { 30304 x: point.x - cursor_image_pixel.x * new_zoom, 30305 y: point.y - cursor_image_pixel.y * new_zoom 30306 }; 30307 if (new_zoom === min_zoom || is_fit_zoom) { 30308 const canvas_width = this.image_editor_context.app.screen.width; 30309 const canvas_height = this.image_editor_context.app.screen.height; 30310 const new_scaled_width = this.local_dimensions.width * new_zoom; 30311 const new_scaled_height = this.local_dimensions.height * new_zoom; 30312 new_container_world_pos.x = (canvas_width - new_scaled_width) / 2; 30313 new_container_world_pos.y = (canvas_height - this.pad_bottom - new_scaled_height) / 2; 30314 } 30315 this.image_editor_context.set_image_properties({ 30316 scale: new_zoom, 30317 position: new_container_world_pos, 30318 animate: typeof hard === "boolean" ? !hard : new_zoom === min_zoom 30319 }); 30320 this.min_zoom.set(new_zoom === min_zoom); 30321 } 30322 /** 30323 * Handles pointer down events 30324 * @param {FederatedPointerEvent} event - The pointer event 30325 * @private 30326 */ 30327 handle_pointer_down(event) { 30328 if (event.button === 0 && this.current_tool === "pan") { 30329 this.is_pointer_dragging = true; 30330 this.drag_start.copyFrom(event.global); 30331 this.drag_start.x -= this.local_position.x; 30332 this.drag_start.y -= this.local_position.y; 30333 } 30334 } 30335 /** 30336 * Handles pointer move events 30337 * @param {FederatedPointerEvent} event - The pointer event 30338 * @private 30339 */ 30340 handle_pointer_move(event) { 30341 if (this.is_pointer_dragging && this.current_tool === "pan") { 30342 const raw_position = { 30343 x: event.global.x - this.drag_start.x, 30344 y: event.global.y - this.drag_start.y 30345 }; 30346 const canvas = this.image_editor_context.app.screen; 30347 const scaled_width = this.local_dimensions.width * this.local_scale; 30348 const scaled_height = this.local_dimensions.height * this.local_scale; 30349 const available_width = canvas.width - this.border_padding * 2; 30350 const available_height = canvas.height - this.border_padding * 2 - this.pad_bottom; 30351 let final_position = { ...raw_position }; 30352 if (scaled_width <= available_width) { 30353 final_position.x = (canvas.width - scaled_width) / 2; 30354 } else { 30355 final_position.x = raw_position.x; 30356 } 30357 if (scaled_height <= available_height) { 30358 final_position.y = (canvas.height - this.pad_bottom - scaled_height) / 2; 30359 } else { 30360 final_position.y = raw_position.y; 30361 } 30362 this.image_editor_context.set_image_properties({ 30363 scale: this.local_scale, 30364 position: final_position 30365 }); 30366 } 30367 } 30368 /** 30369 * Handles pointer up events 30370 * @param {FederatedPointerEvent} event - The pointer event 30371 * @private 30372 */ 30373 handle_pointer_up(event) { 30374 if (this.is_pointer_dragging && this.current_tool === "pan") { 30375 this.is_pointer_dragging = false;
30376 const raw_position = { 30377 x: event.global.x - this.drag_start.x, 30378 y: event.global.y - this.drag_start.y 30379 }; 30380 const canvas = this.image_editor_context.app.screen; 30381 const scaled_width = this.local_dimensions.width * this.local_scale; 30382 const scaled_height = this.local_dimensions.height * this.local_scale; 30383 const available_width = canvas.width - this.border_padding * 2; 30384 const available_height = canvas.height - this.border_padding * 2 - this.pad_bottom; 30385 let final_position = { ...raw_position }; 30386 if (scaled_width <= available_width) { 30387 final_position.x = (canvas.width - scaled_width) / 2; 30388 } else { 30389 const max_offset = canvas.width / 2; 30390 const left_bound = max_offset; 30391 const right_bound = canvas.width - max_offset - scaled_width; 30392 final_position.x = Math.min( 30393 Math.max(raw_position.x, right_bound), 30394 left_bound 30395 ); 30396 } 30397 if (scaled_height <= available_height) { 30398 final_position.y = (canvas.height - this.pad_bottom - scaled_height) / 2; 30399 } else { 30400 const max_offset = (canvas.height - this.pad_bottom) / 2; 30401 const top_bound = max_offset; 30402 const bottom_bound = canvas.height - this.pad_bottom - max_offset - scaled_height; 30403 final_position.y = Math.min( 30404 Math.max(raw_position.y, bottom_bound), 30405 top_bound 30406 ); 30407 } 30408 this.image_editor_context.set_image_properties({ 30409 scale: this.local_scale, 30410 position: final_position, 30411 animate: true 30412 }); 30413 } 30414 } 30415} 30416 30417class LayerManager { 30418 layers = []; 30419 active_layer = null; 30420 active_layer_id = null; 30421 draw_textures = /* @__PURE__ */ new Map(); 30422 layer_store = writable({ 30423 active_layer: "", 30424 layers: [] 30425 }); 30426 background_layer = null; 30427 image_container; 30428 app; 30429 fixed_canvas; 30430 dark; 30431 border_region; 30432 layer_options; 30433 constructor(image_container, app, fixed_canvas, dark, border_region, layer_options) { 30434 this.image_container = image_container; 30435 this.app = app; 30436 this.fixed_canvas = fixed_canvas; 30437 this.dark = dark; 30438 this.border_region = border_region; 30439 this.layer_options = layer_options; 30440 } 30441 toggle_layer_visibility(id) { 30442 const layer = this.layers.find((l) => l.id === id); 30443 if (layer) { 30444 layer.container.visible = !layer.container.visible; 30445 layer.visible = layer.container.visible; 30446 this.layer_store.update((state) => ({ 30447 active_layer: state.active_layer, 30448 layers: this.layers 30449 })); 30450 } 30451 } 30452 create_background_layer(width, height) { 30453 if (this.background_layer) { 30454 this.background_layer.destroy(); 30455 } 30456 const layer = new Container(); 30457 this.background_layer = layer; 30458 const bg_texture = RenderTexture.create({ 30459 width, 30460 height, 30461 resolution: window.devicePixelRatio, 30462 antialias: true, 30463 scaleMode: SCALE_MODES.NEAREST 30464 }); 30465 const bg_sprite = new Sprite(bg_texture); 30466 layer.addChild(bg_sprite); 30467 const clear_graphics = new Graphics(); 30468 clear_graphics.clear(); 30469 clear_graphics.rect(0, 0, width, height).fill({ color: this.dark ? 3355443 : 16777215, alpha: 1 }); 30470 this.app.renderer.render({ 30471 container: clear_graphics, 30472 target: bg_texture, 30473 clear: true 30474 }); 30475 this.image_container.addChild(layer); 30476 layer.zIndex = -1; 30477 this.update_layer_order(); 30478 return layer; 30479 } 30480 set_layer_options(layer_options, width, height) { 30481 this.layer_options = layer_options; 30482 this.reset_layers(width, height); 30483 } 30484 /** 30485 * Creates a background layer from an image URL 30486 * @param url The URL of the image to use as background 30487 * @param width The width of the layer (if fixed_canvas is true) 30488 * @param height The height of the layer (if fixed_canvas is true) 30489 * @param borderRegion Minimum border region to add around the image for outpainting (in pixels) 30490 * @returns A Promise that resolves to the background Container when the image is loaded 30491 */ 30492 async create_background_layer_from_url(url, width, height) { 30493 const layer = this.create_background_layer( 30494 width || this.image_container.width, 30495 height || this.image_container.height 30496 ); 30497 try { 30498 const texture = await Texture.from(url); 30499 const sprite = new Sprite(texture); 30500 const imageWidth = sprite.texture.width; 30501 const imageHeight = sprite.texture.height; 30502 const containerWidth = width || this.image_container.width; 30503 const containerHeight = height || this.image_container.height; 30504 if (this.fixed_canvas) { 30505 const effectiveContainerWidth = Math.max( 30506 containerWidth - this.border_region * 2, 30507 10 30508 ); 30509 const effectiveContainerHeight = Math.max( 30510 containerHeight - this.border_region * 2, 30511 10 30512 ); 30513 const imageAspectRatio = imageWidth / imageHeight; 30514 const containerAspectRatio = effectiveContainerWidth / effectiveContainerHeight; 30515 let finalWidth, finalHeight; 30516 let posX = this.border_region, posY = this.border_region; 30517 if (imageWidth <= effectiveContainerWidth && imageHeight <= effectiveContainerHeight) { 30518 finalWidth = imageWidth; 30519 finalHeight = imageHeight; 30520 } else { 30521 if (imageAspectRatio > containerAspectRatio) { 30522 finalWidth = effectiveContainerWidth; 30523 finalHeight = effectiveContainerWidth / imageAspectRatio; 30524 } else { 30525 finalHeight = effectiveContainerHeight; 30526 finalWidth = effectiveContainerHeight * imageAspectRatio; 30527 } 30528 }
30529 posX += Math.round((effectiveContainerWidth - finalWidth) / 2); 30530 posY += Math.round((effectiveContainerHeight - finalHeight) / 2); 30531 sprite.width = finalWidth; 30532 sprite.height = finalHeight; 30533 sprite.position.set(posX, posY); 30534 } else { 30535 sprite.position.set(this.border_region, this.border_region); 30536 if (this.background_layer) { 30537 this.background_layer.destroy(); 30538 } 30539 const totalWidth = imageWidth + this.border_region * 2; 30540 const totalHeight = imageHeight + this.border_region * 2; 30541 const newLayer = this.create_background_layer(totalWidth, totalHeight); 30542 sprite.width = imageWidth; 30543 sprite.height = imageHeight; 30544 newLayer.addChild(sprite); 30545 return newLayer; 30546 } 30547 layer.addChild(sprite); 30548 return layer; 30549 } catch (error) { 30550 console.error("Error loading image from URL:", error); 30551 return layer; 30552 } 30553 } 30554 create_layer({ 30555 width, 30556 height, 30557 layer_name, 30558 user_created, 30559 layer_id = void 0, 30560 make_active = false 30561 }) { 30562 const layer = new Container(); 30563 const _layer_id = layer_id || Math.random().toString(36).substring(2, 15); 30564 const _layer_name = layer_name || `Layer ${this.layers.length + 1}`; 30565 this.layers.push({ 30566 name: _layer_name, 30567 id: _layer_id, 30568 container: layer, 30569 user_created, 30570 visible: true 30571 }); 30572 this.image_container.addChild(layer); 30573 const draw_texture = RenderTexture.create({ 30574 width, 30575 height, 30576 resolution: window.devicePixelRatio, 30577 antialias: true, 30578 scaleMode: SCALE_MODES.NEAREST 30579 }); 30580 const canvas_sprite = new Sprite(draw_texture); 30581 layer.addChild(canvas_sprite); 30582 const clear_graphics = new Graphics(); 30583 clear_graphics.clear(); 30584 clear_graphics.beginFill(0, 0); 30585 clear_graphics.drawRect(0, 0, width, height); 30586 clear_graphics.endFill(); 30587 this.app.renderer.render({ 30588 container: clear_graphics, 30589 target: draw_texture, 30590 clear: true 30591 }); 30592 this.draw_textures.set(layer, draw_texture); 30593 this.update_layer_order(); 30594 if (make_active) { 30595 this.set_active_layer(_layer_id); 30596 } 30597 this.layer_store.set({ 30598 active_layer: this.active_layer_id || "", 30599 layers: this.layers 30600 }); 30601 return layer; 30602 } 30603 /** 30604 * Creates a new layer with an image loaded from a URL 30605 * @param url The URL of the image to load 30606 * @param borderRegion Minimum border region to add around the image for outpainting (in pixels) 30607 * @returns A Promise that resolves to the layer ID 30608 */ 30609 async add_layer_from_url(url) { 30610 const { width, height } = this.image_container.getLocalBounds(); 30611 const layer = this.create_layer({ 30612 width, 30613 height, 30614 layer_name: "Layer 1", 30615 user_created: true 30616 }); 30617 const layerIndex = this.layers.findIndex((l) => l.container === layer); 30618 if (layerIndex === -1) { 30619 console.error("Could not find newly created layer"); 30620 return ""; 30621 } 30622 const layerId = this.layers[layerIndex].id; 30623 try { 30624 const texture = await Assets.load(url); 30625 const drawTexture = this.draw_textures.get(layer); 30626 if (!drawTexture) { 30627 console.error("No draw texture found for layer"); 30628 return layerId; 30629 } 30630 const sprite = new Sprite(texture); 30631 const imageWidth = sprite.width; 30632 const imageHeight = sprite.height; 30633 let posX = this.border_region; 30634 let posY = this.border_region; 30635 const effectiveWidth = this.fixed_canvas ? width - this.border_region * 2 : width; 30636 const effectiveHeight = this.fixed_canvas ? height - this.border_region * 2 : height; 30637 if (imageWidth < effectiveWidth || imageHeight < effectiveHeight) { 30638 posX = Math.floor((effectiveWidth - imageWidth) / 2); 30639 posY = Math.floor((effectiveHeight - imageHeight) / 2); 30640 } 30641 sprite.position.set(posX, posY); 30642 if (imageWidth > effectiveWidth || imageHeight > effectiveHeight) { 30643 const imageAspectRatio = imageWidth / imageHeight; 30644 const areaAspectRatio = effectiveWidth / effectiveHeight; 30645 let finalWidth, finalHeight; 30646 if (imageAspectRatio > areaAspectRatio) { 30647 finalWidth = effectiveWidth; 30648 finalHeight = effectiveWidth / imageAspectRatio; 30649 } else { 30650 finalHeight = effectiveHeight; 30651 finalWidth = effectiveHeight * imageAspectRatio; 30652 }
30653 sprite.width = finalWidth; 30654 sprite.height = finalHeight; 30655 posX = this.border_region + Math.floor((effectiveWidth - finalWidth) / 2); 30656 posY = this.border_region + Math.floor((effectiveHeight - finalHeight) / 2); 30657 sprite.position.set(posX, posY); 30658 } 30659 this.app.renderer.render(sprite, { renderTexture: drawTexture }); 30660 this.set_active_layer(layerId); 30661 return layerId; 30662 } catch (error) { 30663 console.error("Error loading image from URL:", error); 30664 return layerId; 30665 } 30666 } 30667 get_active_layer() { 30668 return this.active_layer; 30669 } 30670 set_active_layer(id) { 30671 if (this.layers.some((l) => l.id === id)) { 30672 this.active_layer = this.layers.find((l) => l.id === id)?.container || this.layers[0]?.container || null; 30673 this.active_layer_id = id; 30674 this.layer_store.set({ 30675 active_layer: id, 30676 layers: this.layers 30677 }); 30678 } 30679 } 30680 get_layers() { 30681 return this.layers; 30682 } 30683 get_layer_textures(id) { 30684 const layer = this.layers.find((l) => l.id === id); 30685 if (layer) { 30686 const draw = this.draw_textures.get(layer.container); 30687 if (draw) { 30688 return { draw }; 30689 } 30690 } 30691 return null; 30692 } 30693 delete_layer(id) { 30694 const index = this.layers.findIndex((l) => l.id === id); 30695 if (index > -1) { 30696 const draw_texture = this.draw_textures.get(this.layers[index].container); 30697 if (draw_texture) { 30698 draw_texture.destroy(); 30699 this.draw_textures.delete(this.layers[index].container); 30700 } 30701 this.layers[index].container.destroy(); 30702 if (this.active_layer === this.layers[index].container) { 30703 const new_active_layer = this.layers[Math.max(0, index - 1)] || null; 30704 this.active_layer = new_active_layer?.container || null; 30705 this.active_layer_id = new_active_layer?.id || null; 30706 } 30707 this.layers = this.layers.filter((l) => l.id !== id); 30708 this.layer_store.update((_layers) => ({ 30709 active_layer: _layers.active_layer === id ? this.layers[this.layers.length - 1]?.id : _layers.active_layer, 30710 layers: this.layers 30711 })); 30712 this.update_layer_order(); 30713 } 30714 } 30715 update_layer_order() { 30716 if (this.background_layer) { 30717 this.background_layer.zIndex = -1; 30718 } 30719 this.layers.forEach((layer, index) => { 30720 layer.container.zIndex = index; 30721 }); 30722 } 30723 move_layer(id, direction) { 30724 const index = this.layers.findIndex((l) => l.id === id); 30725 if (index > -1) { 30726 const new_index = direction === "up" ? index - 1 : index + 1; 30727 this.layers = this.layers.map((l, i) => { 30728 if (i === index) { 30729 return this.layers[new_index]; 30730 } 30731 if (i === new_index) { 30732 return this.layers[index]; 30733 } 30734 return l; 30735 }); 30736 this.update_layer_order(); 30737 this.layer_store.update((_layers) => ({ 30738 active_layer: id, 30739 layers: this.layers 30740 })); 30741 } 30742 } 30743 /** 30744 * Resizes all layers to a new width and height 30745 * @param newWidth The new width of the layers 30746 * @param newHeight The new height of the layers 30747 * @param scale If true, scales the layer content to fit the new dimensions. If false, keeps the content size unchanged. 30748 * @param anchor The anchor point to position the content relative to the new size 30749 */ 30750 resize_all_layers(newWidth, newHeight, scale, anchor, oldCanvasWidth, oldCanvasHeight) { 30751 new Map(this.layers.map((l) => [l.id, l])); 30752 const oldBackgroundLayer = this.background_layer; 30753 const calculateOffset = () => { 30754 let offsetX = 0; 30755 let offsetY = 0; 30756 const deltaWidth = newWidth - oldCanvasWidth; 30757 const deltaHeight = newHeight - oldCanvasHeight; 30758 if (anchor.includes("left")) { 30759 offsetX = 0; 30760 } else if (anchor.includes("right")) { 30761 offsetX = deltaWidth; 30762 } else { 30763 offsetX = Math.floor(deltaWidth / 2); 30764 } 30765 if (anchor.includes("top")) { 30766 offsetY = 0; 30767 } else if (anchor.includes("bottom")) { 30768 offsetY = deltaHeight; 30769 } else {
30770 offsetY = Math.floor(deltaHeight / 2); 30771 } 30772 return { offsetX, offsetY }; 30773 }; 30774 this.background_layer = this._resize_background_layer( 30775 oldBackgroundLayer, 30776 newWidth, 30777 newHeight, 30778 scale, 30779 calculateOffset 30780 ); 30781 const processedLayers = []; 30782 const oldLayerData = this.layers.map((oldLayer) => ({ 30783 id: oldLayer.id, 30784 name: oldLayer.name, 30785 user_created: oldLayer.user_created, 30786 texture: this.draw_textures.get(oldLayer.container), 30787 container: oldLayer.container 30788 })); 30789 this.layers = []; 30790 this.draw_textures.clear(); 30791 for (const oldData of oldLayerData) { 30792 const newLayer = this._resize_single_layer( 30793 oldData, 30794 newWidth, 30795 newHeight, 30796 scale, 30797 calculateOffset 30798 ); 30799 if (newLayer) { 30800 processedLayers.push(newLayer); 30801 } 30802 } 30803 const currentActiveId = get$1(this.layer_store).active_layer; 30804 const activeLayerExists = processedLayers.some( 30805 (l) => l.id === currentActiveId 30806 ); 30807 if (!activeLayerExists && processedLayers.length > 0) { 30808 this.set_active_layer(processedLayers[0].id); 30809 } else if (processedLayers.length === 0) { 30810 this.layer_store.update((s) => ({ ...s, active_layer: "" })); 30811 } else { 30812 this.layer_store.update((s) => ({ ...s })); 30813 } 30814 this.update_layer_order(); 30815 setTimeout(() => { 30816 Assets.cache.reset(); 30817 this.app.renderer.textureGC.run(); 30818 }, 100); 30819 } 30820 /** 30821 * Helper method to resize the background layer. 30822 */ 30823 _resize_background_layer(oldBackgroundLayer, newWidth, newHeight, scale, calculateOffset) { 30824 if (!oldBackgroundLayer) { 30825 return this.create_background_layer(newWidth, newHeight); 30826 } 30827 let backgroundImage = oldBackgroundLayer.children.find( 30828 (child) => child instanceof Sprite && child.texture !== oldBackgroundLayer.children[0]?.texture 30829 ); 30830 const newBackgroundLayer = this.create_background_layer( 30831 newWidth, 30832 newHeight 30833 ); 30834 if (backgroundImage) { 30835 const newBgImage = new Sprite(backgroundImage.texture); 30836 newBgImage.width = backgroundImage.width; 30837 newBgImage.height = backgroundImage.height; 30838 if (scale) { 30839 newBgImage.width = newWidth; 30840 newBgImage.height = newHeight; 30841 newBgImage.position.set(0, 0); 30842 } else { 30843 const { offsetX, offsetY } = calculateOffset(); 30844 newBgImage.position.set( 30845 backgroundImage.x + offsetX, 30846 backgroundImage.y + offsetY 30847 ); 30848 } 30849 newBackgroundLayer.addChild(newBgImage); 30850 } 30851 return newBackgroundLayer; 30852 } 30853 /** 30854 * Helper method to resize a single regular layer. 30855 */ 30856 _resize_single_layer(oldData, newWidth, newHeight, scale, calculateOffset) { 30857 if (!oldData.texture) { 30858 console.warn( 30859 `No texture found for layer ${oldData.id}, skipping cleanup.` 30860 ); 30861 if (oldData.container && !oldData.container.destroyed) { 30862 if (this.image_container.children.includes(oldData.container)) { 30863 this.image_container.removeChild(oldData.container); 30864 } 30865 oldData.container.destroy({ children: true }); 30866 } 30867 return null; 30868 } 30869 const newContainer = this.create_layer({ 30870 width: newWidth, 30871 height: newHeight, 30872 layer_name: oldData.name, 30873 user_created: oldData.user_created 30874 }); 30875 const newLayer = this.layers[this.layers.length - 1]; 30876 newLayer.id = oldData.id; 30877 const newTexture = this.draw_textures.get(newContainer); 30878 if (!newTexture) { 30879 console.error( 30880 `Failed to get texture for newly created layer ${newLayer.id}. Cleaning up.` 30881 ); 30882 if (newContainer && !newContainer.destroyed) { 30883 if (this.image_container.children.includes(newContainer)) { 30884 this.image_container.removeChild(newContainer); 30885 } 30886 newContainer.destroy({ children: true }); 30887 const idx = this.layers.findIndex((l) => l.container === newContainer); 30888 if (idx > -1) this.layers.splice(idx, 1); 30889 this.draw_textures.delete(newContainer); 30890 } 30891 if (oldData.texture && !oldData.texture.destroyed) 30892 oldData.texture.destroy(true);
30893 if (oldData.container && !oldData.container.destroyed) 30894 oldData.container.destroy({ children: true }); 30895 return null; 30896 } 30897 this.app.renderer.clear({ target: newTexture, clearColor: [0, 0, 0, 0] }); 30898 const sprite = new Sprite(oldData.texture); 30899 if (scale) { 30900 sprite.width = newWidth; 30901 sprite.height = newHeight; 30902 sprite.position.set(0, 0); 30903 } else { 30904 const { offsetX, offsetY } = calculateOffset(); 30905 sprite.position.set(offsetX, offsetY); 30906 } 30907 this.app.renderer.render(sprite, { renderTexture: newTexture }); 30908 sprite.destroy(); 30909 if (oldData.texture && !oldData.texture.destroyed) { 30910 oldData.texture.destroy(true); 30911 } 30912 if (oldData.container && !oldData.container.destroyed) { 30913 if (this.image_container.children.includes(oldData.container)) { 30914 this.image_container.removeChild(oldData.container); 30915 } 30916 oldData.container.destroy({ children: true }); 30917 } 30918 return newLayer; 30919 } 30920 async get_blobs(width, height) { 30921 const blobs = { 30922 background: await get_canvas_blob( 30923 this.app.renderer, 30924 this.background_layer, 30925 { 30926 width, 30927 height, 30928 x: 0, 30929 y: 0 30930 } 30931 ), 30932 layers: await Promise.all( 30933 this.layers.map(async (layer) => { 30934 const blob = await get_canvas_blob( 30935 this.app.renderer, 30936 layer.container, 30937 { 30938 width, 30939 height, 30940 x: 0, 30941 y: 0 30942 } 30943 ); 30944 if (blob) { 30945 return blob; 30946 } 30947 return null; 30948 }) 30949 ), 30950 composite: await get_canvas_blob( 30951 this.app.renderer, 30952 this.image_container, 30953 { 30954 width, 30955 height, 30956 x: 0, 30957 y: 0 30958 } 30959 ) 30960 }; 30961 return blobs; 30962 } 30963 reset_layers(width, height, persist = false) { 30964 const _layers_to_recreate = persist ? this.layers.map((layer) => [layer.name, layer.id]) : this.layer_options.layers.map((layer) => [layer, void 0]); 30965 this.layers.forEach((layer) => { 30966 this.delete_layer(layer.id); 30967 }); 30968 for (const [layer_name, layer_id] of _layers_to_recreate) { 30969 this.create_layer({ 30970 width, 30971 height, 30972 layer_name, 30973 user_created: this.layer_options.layers.find((l) => l === layer_name) ? false : true, 30974 layer_id 30975 }); 30976 } 30977 if (!persist) {
30978 this.active_layer = this.layers[0].container; 30979 this.active_layer_id = this.layers[0].id; 30980 } else { 30981 this.active_layer = this.layers.find((l) => l.id === this.active_layer_id)?.container || this.layers[0]?.container; 30982 if (!this.active_layer) return; 30983 } 30984 this.layer_store.update((state) => ({ 30985 active_layer: this.active_layer_id || this.layers[0].id, 30986 layers: this.layers 30987 })); 30988 } 30989 init_layers(width, height) { 30990 for (const layer of this.layers) { 30991 this.delete_layer(layer.id); 30992 } 30993 let i = 0; 30994 for (const layer of this.layer_options.layers) { 30995 this.create_layer({ 30996 width, 30997 height, 30998 layer_name: layer, 30999 user_created: false, 31000 layer_id: `layer-${i}` 31001 }); 31002 i++; 31003 } 31004 this.active_layer = this.layers[0].container; 31005 this.active_layer_id = this.layers[0].id; 31006 this.layer_store.update((_layers) => ({ 31007 active_layer: this.layers[0].id, 31008 layers: this.layers 31009 })); 31010 } 31011} 31012class AddLayerCommand { 31013 constructor(context, options) { 31014 this.context = context; 31015 this.width = options.width; 31016 this.height = options.height; 31017 this.layer_name = options.layer_name || `Layer ${this.context.layer_manager.get_layers().length + 1}`; 31018 this.user_created = options.user_created; 31019 this.layer_id = options.layer_id || Math.random().toString(36).substring(2, 15); 31020 this.make_active = options.make_active || false; 31021 this.name = "AddLayer"; 31022 const current_layers = this.context.layer_manager.get_layers(); 31023 const current_active = current_layers.find( 31024 (l) => l.container === this.context.layer_manager.get_active_layer() 31025 ); 31026 this.previous_active_layer = current_active?.id || null; 31027 } 31028 layer_id; 31029 layer_name; 31030 width; 31031 height; 31032 user_created; 31033 make_active; 31034 previous_active_layer = null; 31035 name; 31036 async execute(context) { 31037 if (context) { 31038 this.context = context; 31039 } 31040 this.context.layer_manager.create_layer({ 31041 width: this.width, 31042 height: this.height, 31043 layer_name: this.layer_name, 31044 user_created: this.user_created, 31045 layer_id: this.layer_id, 31046 make_active: this.make_active 31047 }); 31048 } 31049 async undo() { 31050 this.context.layer_manager.delete_layer(this.layer_id); 31051 if (this.previous_active_layer) { 31052 this.context.layer_manager.set_active_layer(this.previous_active_layer); 31053 } 31054 } 31055} 31056class RemoveLayerCommand { 31057 constructor(context, layer_id) { 31058 this.context = context; 31059 this.name = "RemoveLayer"; 31060 const layers = this.context.layer_manager.get_layers(); 31061 const layer_to_remove = layers.find((l) => l.id === layer_id); 31062 if (!layer_to_remove) { 31063 throw new Error(`Layer with ID ${layer_id} not found`); 31064 } 31065 const active_layer = this.context.layer_manager.get_active_layer(); 31066 const was_active = layer_to_remove.container === active_layer; 31067 if (was_active) { 31068 const layer_index = layers.findIndex((l) => l.id === layer_id); 31069 const next_active = layers[Math.max(0, layer_index - 1)]; 31070 this.previous_active_layer = next_active?.id || null; 31071 } 31072 this.layer_data = { 31073 id: layer_to_remove.id, 31074 name: layer_to_remove.name, 31075 user_created: layer_to_remove.user_created, 31076 visible: layer_to_remove.visible, 31077 was_active 31078 }; 31079 this.captureTextureData(layer_id); 31080 } 31081 layer_data; 31082 previous_active_layer = null; 31083 texture_copy = null; 31084 name; 31085 /** 31086 * Create a direct copy of the layer's texture 31087 */ 31088 captureTextureData(layer_id) { 31089 const layer_textures = this.context.layer_manager.get_layer_textures(layer_id); 31090 if (!layer_textures) return; 31091 try { 31092 const original_texture = layer_textures.draw; 31093 const texture_copy = RenderTexture.create({ 31094 width: original_texture.width, 31095 height: original_texture.height, 31096 resolution: window.devicePixelRatio || 1 31097 }); 31098 const sprite = new Sprite(original_texture); 31099 this.context.app.renderer.render(sprite, { renderTexture: texture_copy }); 31100 this.texture_copy = texture_copy; 31101 sprite.destroy(); 31102 } catch (e) { 31103 console.error("Failed to copy layer texture:", e); 31104 this.texture_copy = null; 31105 } 31106 } 31107 async execute(context) { 31108 if (context) { 31109 this.context = context; 31110 } 31111 this.context.layer_manager.delete_layer(this.layer_data.id); 31112 } 31113 async undo() { 31114 const dimensions = get$1(this.context.dimensions); 31115 this.context.layer_manager.create_layer({ 31116 width: dimensions.width, 31117 height: dimensions.height, 31118 layer_name: this.layer_data.name, 31119 user_created: this.layer_data.user_created,
31120 layer_id: this.layer_data.id, 31121 make_active: this.layer_data.was_active 31122 }); 31123 if (this.texture_copy) { 31124 try { 31125 const layer_textures = this.context.layer_manager.get_layer_textures( 31126 this.layer_data.id 31127 ); 31128 if (layer_textures) { 31129 const sprite = new Sprite(this.texture_copy); 31130 this.context.app.renderer.render(sprite, { 31131 renderTexture: layer_textures.draw 31132 }); 31133 sprite.destroy(); 31134 } 31135 } catch (e) { 31136 console.error("Failed to restore layer content:", e); 31137 } 31138 } 31139 if (!this.layer_data.visible) { 31140 this.context.layer_manager.toggle_layer_visibility(this.layer_data.id); 31141 } 31142 if (!this.layer_data.was_active && this.previous_active_layer) { 31143 this.context.layer_manager.set_active_layer(this.previous_active_layer); 31144 } 31145 } 31146 /** 31147 * Clean up resources when the command is no longer needed 31148 * This would need to be called by a garbage collection mechanism 31149 */ 31150 destroy() { 31151 if (this.texture_copy) { 31152 this.texture_copy.destroy(); 31153 this.texture_copy = null; 31154 } 31155 } 31156} 31157class ReorderLayerCommand { 31158 constructor(context, layer_id, direction) { 31159 this.context = context; 31160 this.layer_id = layer_id; 31161 this.direction = direction; 31162 this.name = "ReorderLayer"; 31163 const layers = this.context.layer_manager.get_layers(); 31164 this.original_order = layers.map((l) => l.id); 31165 const index = layers.findIndex((l) => l.id === layer_id); 31166 if (index === -1) { 31167 throw new Error(`Layer with ID ${layer_id} not found`); 31168 } 31169 const new_index = direction === "up" ? index - 1 : index + 1; 31170 if (new_index < 0 || new_index >= layers.length) { 31171 this.new_order = [...this.original_order]; 31172 } else { 31173 this.new_order = [...this.original_order]; 31174 [this.new_order[index], this.new_order[new_index]] = [ 31175 this.new_order[new_index], 31176 this.new_order[index] 31177 ]; 31178 } 31179 } 31180 original_order; 31181 new_order; 31182 layer_id; 31183 direction; 31184 name; 31185 async execute(context) { 31186 if (context) { 31187 this.context = context; 31188 } 31189 this.context.layer_manager.move_layer(this.layer_id, this.direction); 31190 } 31191 async undo() { 31192 const current_layers = this.context.layer_manager.get_layers(); 31193 if (this.original_order.join(",") === current_layers.map((l) => l.id).join(",")) { 31194 return; 31195 } 31196 const layer_index = current_layers.findIndex((l) => l.id === this.layer_id); 31197 if (layer_index === -1) return; 31198 const original_index = this.original_order.indexOf(this.layer_id); 31199 const move_direction = layer_index > original_index ? "up" : "down"; 31200 this.context.layer_manager.move_layer(this.layer_id, move_direction); 31201 } 31202} 31203 31204const core_tool_map = { 31205 image: () => new ImageTool(), 31206 zoom: () => new ZoomTool() 31207}; 31208const spring_config = { 31209 stiffness: 0.45, 31210 damping: 0.8 31211}; 31212class EditorState { 31213 state; 31214 scale; 31215 position; 31216 subscribers; 31217 constructor(editor) { 31218 if (!(editor instanceof ImageEditor)) { 31219 throw new Error("EditorState must be created by ImageEditor"); 31220 } 31221 this.scale = 1; 31222 this.position = { x: 0, y: 0 }; 31223 this.subscribers = /* @__PURE__ */ new Set(); 31224 this.state = Object.freeze({ 31225 get position() { 31226 return { ...this.position }; 31227 }, 31228 get scale() { 31229 return this.scale; 31230 }, 31231 get current_tool() { 31232 return this.current_tool; 31233 }, 31234 get current_subtool() { 31235 return this.current_subtool; 31236 }, 31237 subscribe: this.subscribe.bind(this) 31238 }); 31239 } 31240 _set_position(x, y) { 31241 const oldPosition = { ...this.position }; 31242 this.position = { x, y }; 31243 this._notify_subscribers("position", oldPosition, this.position); 31244 } 31245 _set_scale(scale) { 31246 const oldScale = this.scale; 31247 this.scale = scale; 31248 this._notify_subscribers("scale", oldScale, scale); 31249 } 31250 _notify_subscribers(property, oldValue, newValue) { 31251 this.subscribers.forEach((callback) => { 31252 callback({ 31253 property, 31254 oldValue, 31255 newValue, 31256 timestamp: Date.now() 31257 }); 31258 }); 31259 } 31260 subscribe(callback) { 31261 this.subscribers.add(callback); 31262 return () => this.subscribers.delete(callback); 31263 } 31264} 31265class ImageEditor { 31266 ready; 31267 background_image_present = writable(false); 31268 min_zoom = writable(true); 31269 app; 31270 ui_container; 31271 image_container; 31272 command_manager; 31273 layer_manager; 31274 tools = /* @__PURE__ */ new Map(); 31275 current_tool; 31276 current_subtool; 31277 target_element; 31278 width; 31279 height; 31280 original_width; 31281 original_height; 31282 dimensions; 31283 scale; 31284 position; 31285 state; 31286 scale_value = 1; 31287 position_value = { x: 0, y: 0 }; 31288 dimensions_value = { 31289 width: 0, 31290 height: 0 31291 }; 31292 layers = writable({ 31293 active_layer: "", 31294 layers: [] 31295 }); 31296 outline_container; 31297 outline_graphics; 31298 background_image; 31299 ready_resolve; 31300 event_callbacks = /* @__PURE__ */ new Map(); 31301 fixed_canvas; 31302 dark; 31303 border_region; 31304 layer_options; 31305 overlay_container; 31306 overlay_graphics; 31307 pad_bottom; 31308 theme_mode; 31309 constructor(options) { 31310 this.pad_bottom = options.pad_bottom || 0; 31311 this.dark = options.dark || false; 31312 this.theme_mode = options.theme_mode || "dark"; 31313 this.target_element = options.target_element; 31314 this.width = options.width; 31315 this.height = options.height; 31316 this.original_width = options.width; 31317 this.original_height = options.height; 31318 this.command_manager = new CommandManager(); 31319 this.ready = new Promise((resolve) => { 31320 this.ready_resolve = resolve; 31321 }); 31322 this.fixed_canvas = options.fixed_canvas || false; 31323 this.tools = new Map( 31324 options.tools.map((tool) => { 31325 if (typeof tool === "string") { 31326 return [tool, core_tool_map[tool]()]; 31327 } 31328 return [tool.name, tool]; 31329 }) 31330 ); 31331 for (const tool of this.tools.values()) { 31332 if (tool?.on) { 31333 tool.on("change", () => { 31334 this.notify("change"); 31335 }); 31336 } 31337 } 31338 this.dimensions = spring( 31339 { width: this.width, height: this.height }, 31340 spring_config 31341 ); 31342 this.scale = spring(1, spring_config); 31343 this.position = spring({ x: 0, y: 0 }, spring_config); 31344 this.state = new EditorState(this); 31345 this.border_region = options.border_region || 0; 31346 this.layer_options = options.layer_options || { 31347 allow_additional_layers: true, 31348 layers: ["Layer 1"], 31349 disabled: false 31350 }; 31351 this.scale.subscribe((scale) => { 31352 this.state._set_scale(scale); 31353 }); 31354 this.position.subscribe((position) => { 31355 this.state._set_position(position.x, position.y); 31356 }); 31357 this.init(); 31358 } 31359 get context() { 31360 const editor = this; 31361 return { 31362 app: this.app, 31363 ui_container: this.ui_container, 31364 image_container: this.image_container, 31365 get background_image() { 31366 return editor.background_image; 31367 }, 31368 pad_bottom: this.pad_bottom, 31369 command_manager: this.command_manager, 31370 layer_manager: this.layer_manager, 31371 dimensions: { subscribe: this.dimensions.subscribe }, 31372 scale: { subscribe: this.scale.subscribe }, 31373 position: { subscribe: this.position.subscribe }, 31374 set_image_properties: this.set_image_properties.bind(this), 31375 execute_command: this.execute_command.bind(this), 31376 resize_canvas: this.resize_canvas.bind(this), 31377 reset: this.reset.bind(this), 31378 set_background_image: this.set_background_image.bind(this) 31379 }; 31380 }
31381 async init() { 31382 const container_box = this.target_element.getBoundingClientRect(); 31383 new ResizeObserver((entries) => { 31384 entries.forEach((entry) => { 31385 this.resize_canvas( 31386 entry.contentBoxSize[0].inlineSize, 31387 entry.contentBoxSize[0].blockSize 31388 ); 31389 }); 31390 }).observe(this.target_element); 31391 this.app = new Application(); 31392 if (!this.dark) { 31393 globalThis.__PIXI_APP__ = this.app; 31394 } 31395 await this.app.init({ 31396 width: container_box.width, 31397 height: container_box.height, 31398 backgroundAlpha: this.dark ? 0 : 1, 31399 backgroundColor: this.theme_mode === "dark" ? "#27272a" : "#ffffff", 31400 resolution: window.devicePixelRatio, 31401 autoDensity: true, 31402 antialias: true, 31403 powerPreference: "high-performance" 31404 }); 31405 const canvas = this.app.canvas; 31406 canvas.style.background = "transparent"; 31407 this.setup_containers(); 31408 this.layer_manager.create_background_layer(this.width, this.height); 31409 this.layer_manager.init_layers(this.width, this.height); 31410 for (const tool of this.tools.values()) { 31411 await tool.setup(this.context, this.current_tool, this.current_subtool); 31412 } 31413 const zoom = this.tools.get("zoom"); 31414 if (zoom) { 31415 zoom.min_zoom.subscribe((is_min_zoom) => { 31416 this.min_zoom.set(is_min_zoom); 31417 }); 31418 } 31419 this.target_element.appendChild(canvas); 31420 this.target_element.style.background = "transparent"; 31421 this.dimensions.subscribe((dimensions) => { 31422 this.dimensions_value = dimensions; 31423 this.image_container.width = dimensions.width; 31424 this.image_container.height = dimensions.height; 31425 }); 31426 this.scale.subscribe((scale) => { 31427 this.scale_value = scale; 31428 }); 31429 this.position.subscribe((position) => { 31430 this.position_value = position; 31431 }); 31432 this.app.ticker.add(() => { 31433 this.image_container.scale.set(this.scale_value); 31434 this.image_container.position.set( 31435 this.position_value.x, 31436 this.position_value.y 31437 ); 31438 const effective_width = this.dimensions_value.width * this.scale_value; 31439 const effective_height = this.dimensions_value.height * this.scale_value; 31440 const local_x = Math.round( 31441 this.image_container.position.x - this.outline_container.position.x 31442 ); 31443 const local_y = Math.round( 31444 this.image_container.position.y - this.outline_container.position.y 31445 ); 31446 this.overlay_container.position.set( 31447 this.outline_container.position.x, 31448 this.outline_container.position.y 31449 ); 31450 this.outline_graphics.clear(); 31451 this.outline_graphics.rect(local_x, local_y, effective_width, effective_height).fill({ 31452 color: this.dark ? 3355443 : 16777215, 31453 alpha: 1 31454 }); 31455 if (this.border_region > 0) { 31456 const scaled_border = this.border_region * this.scale_value; 31457 const border_x = local_x + scaled_border - 1; 31458 const border_y = local_y + scaled_border - 1; 31459 const border_width = effective_width - scaled_border * 2 + 1; 31460 const border_height = effective_height - scaled_border * 2 + 1; 31461 this.overlay_graphics.clear(); 31462 this.overlay_graphics.rect(border_x, border_y, border_width, border_height).stroke({ 31463 color: 10066329, 31464 width: 1, 31465 alpha: 0, 31466 pixelLine: true 31467 }); 31468 const dashLength = 5; 31469 const gapLength = 5; 31470 const totalLength = dashLength + gapLength; 31471 const lineColor = 10066329; 31472 for (let x = border_x; x < border_x + border_width; x += totalLength) { 31473 this.overlay_graphics.rect( 31474 x, 31475 border_y, 31476 Math.min(dashLength, border_x + border_width - x), 31477 1 31478 ).fill({ color: lineColor, alpha: 0.7 }); 31479 this.overlay_graphics.rect( 31480 x, 31481 border_y + border_height, 31482 Math.min(dashLength, border_x + border_width - x), 31483 1 31484 ).fill({ color: lineColor, alpha: 0.7 }); 31485 } 31486 for (let y = border_y; y < border_y + border_height; y += totalLength) { 31487 this.overlay_graphics.rect( 31488 border_x, 31489 y, 31490 1, 31491 Math.min(dashLength, border_y + border_height - y) 31492 ).fill({ color: lineColor, alpha: 0.7 }); 31493 this.overlay_graphics.rect( 31494 border_x + border_width, 31495 y, 31496 1, 31497 Math.min(dashLength, border_y + border_height - y) 31498 ).fill({ color: lineColor, alpha: 0.7 }); 31499 } 31500 } else { 31501 this.overlay_graphics.clear(); 31502 } 31503 }); 31504 this.ready_resolve(); 31505 } 31506 setup_containers() { 31507 this.image_container = new Container({ 31508 eventMode: "static", 31509 sortableChildren: true 31510 }); 31511 this.image_container.width = this.width; 31512 this.image_container.height = this.height; 31513 this.app.stage.sortableChildren = true; 31514 this.app.stage.alpha = 1; 31515 this.app.stage.addChild(this.image_container); 31516 this.image_container.scale.set(1); 31517 this.ui_container = new Container({ 31518 eventMode: "static" 31519 }); 31520 this.app.stage.addChild(this.ui_container); 31521 this.ui_container.width = this.width; 31522 this.ui_container.height = this.height; 31523 this.outline_container = new Container(); 31524 this.outline_container.zIndex = -10; 31525 this.outline_graphics = new Graphics(); 31526 this.outline_graphics.rect(0, 0, this.width, this.height).fill({ 31527 color: this.dark ? 3355443 : 16777215, 31528 alpha: 0 31529 }); 31530 if (!this.dark) { 31531 const blurFilter = new DropShadowFilter({ 31532 alpha: 0.1, 31533 blur: 2, 31534 color: 0, 31535 offset: { x: 0, y: 0 }, 31536 quality: 4, 31537 shadowOnly: false 31538 }); 31539 this.outline_graphics.filters = [blurFilter]; 31540 } 31541 this.outline_container.addChild(this.outline_graphics); 31542 this.app.stage.addChild(this.outline_container); 31543 this.overlay_container = new Container(); 31544 this.app.stage.addChild(this.overlay_container); 31545 this.overlay_container.width = this.width; 31546 this.overlay_container.height = this.height; 31547 this.overlay_container.zIndex = 999; 31548 this.overlay_container.eventMode = "static"; 31549 this.overlay_container.interactiveChildren = true; 31550 this.overlay_graphics = new Graphics(); 31551 this.overlay_container.addChild(this.overlay_graphics); 31552 this.overlay_graphics.rect(0, 0, this.width, this.height).fill({ 31553 color: 0, 31554 alpha: 0 31555 }); 31556 const app_center_x = this.app.screen.width / 2; 31557 const app_center_y = this.app.screen.height / 2; 31558 this.image_container.position.set(app_center_x, app_center_y); 31559 this.outline_container.position.set(app_center_x, app_center_y); 31560 this.layer_manager = new LayerManager( 31561 this.image_container, 31562 this.app, 31563 this.fixed_canvas, 31564 this.dark, 31565 this.border_region, 31566 this.layer_options 31567 ); 31568 this.layers = this.layer_manager.layer_store; 31569 } 31570 resize_canvas(width, height) { 31571 if (this.app.renderer) { 31572 this.app.renderer.resize(width, height); 31573 } 31574 const app_center_x = width / 2; 31575 const app_center_y = height / 2; 31576 if (this.image_container) { 31577 this.image_container.position.set(app_center_x, app_center_y); 31578 } 31579 if (this.outline_container) { 31580 this.outline_container.position.set(app_center_x, app_center_y); 31581 } 31582 } 31583 reset() { 31584 const zoom = this.tools.get("zoom"); 31585 if (zoom) { 31586 zoom.cleanup(); 31587 zoom.setup(this.context, this.current_tool, this.current_subtool); 31588 } 31589 const brush = this.tools.get("brush"); 31590 if (brush) { 31591 this.set_tool("draw"); 31592 } 31593 } 31594 async set_image_properties(properties) { 31595 let hard = typeof properties.animate !== "undefined" ? !properties.animate : true; 31596 if (properties.position) { 31597 const pos = this.position.set(properties.position, { hard }); 31598 if (hard) { 31599 await pos; 31600 } 31601 } 31602 if (properties.scale) { 31603 const scale = this.scale.set(properties.scale, { hard }); 31604 if (hard) { 31605 await scale; 31606 } 31607 } 31608 if (properties.width && properties.height) { 31609 this.width = properties.width; 31610 this.height = properties.height; 31611 const dimensions = this.dimensions.set( 31612 { width: properties.width, height: properties.height }, 31613 { hard } 31614 ); 31615 if (hard) { 31616 await dimensions; 31617 } 31618 } 31619 } 31620 async execute_command(command) { 31621 await this.command_manager.execute(command, this.context); 31622 } 31623 undo() { 31624 this.command_manager.undo(); 31625 this.notify("change"); 31626 } 31627 redo() { 31628 this.command_manager.redo(this.context); 31629 this.notify("change"); 31630 } 31631 async add_image({ 31632 image, 31633 resize = true 31634 }) { 31635 const image_tool = this.tools.get("image"); 31636 await image_tool.add_image({ 31637 image, 31638 fixed_canvas: this.fixed_canvas, 31639 border_region: this.border_region 31640 }); 31641 } 31642 /** 31643 * Adds an image from a URL as the background layer 31644 * @param url The URL of the image to add 31645 */ 31646 async add_image_from_url(url) { 31647 const image_tool = this.tools.get("image"); 31648 const texture = await Assets.load(url); 31649 await image_tool.add_image({ 31650 image: texture, 31651 fixed_canvas: this.fixed_canvas, 31652 border_region: this.border_region 31653 }); 31654 const resize_tool = this.tools.get("resize"); 31655 if (resize_tool && typeof resize_tool.set_border_region === "function") { 31656 resize_tool.set_border_region(this.border_region); 31657 } 31658 } 31659 set_tool(tool) { 31660 this.current_tool = tool; 31661 for (const tool2 of this.tools.values()) { 31662 tool2.set_tool(this.current_tool, this.current_subtool); 31663 } 31664 } 31665 set_subtool(subtool) { 31666 this.current_subtool = subtool; 31667 for (const tool of this.tools.values()) { 31668 tool.set_tool(this.current_tool, this.current_subtool); 31669 } 31670 } 31671 set_background_image(image) { 31672 this.background_image = image; 31673 } 31674 async reset_canvas() { 31675 this.width = this.original_width; 31676 this.height = this.original_height; 31677 this.layer_manager.reset_layers(this.width, this.height); 31678 this.background_image = void 0; 31679 this.background_image_present.set(false); 31680 this.command_manager.reset(); 31681 await this.set_image_properties({ 31682 width: this.width, 31683 height: this.height, 31684 scale: 1, 31685 position: { x: 0, y: 0 }, 31686 animate: false 31687 }); 31688 this.layer_manager.create_background_layer(this.width, this.height); 31689 for (const tool of this.tools.values()) { 31690 tool.cleanup(); 31691 tool.setup(this.context, this.current_tool, this.current_subtool); 31692 } 31693 const zoom_tool = this.tools.get("zoom"); 31694 if (zoom_tool) { 31695 zoom_tool.min_zoom.subscribe((is_min_zoom) => { 31696 this.min_zoom.set(is_min_zoom); 31697 }); 31698 } 31699 this.notify("change"); 31700 } 31701 add_layer() {
31702 const add_layer_command = new AddLayerCommand(this.context, { 31703 width: this.width, 31704 height: this.height, 31705 user_created: true, 31706 make_active: true 31707 }); 31708 this.execute_command(add_layer_command); 31709 this.notify("change"); 31710 } 31711 /** 31712 * Adds a new layer with an image loaded from a URL 31713 * @param layer_urls The URLs of the images to load 31714 * @returns A Promise that resolves when all layers are added 31715 */ 31716 async add_layers_from_url(layer_urls) { 31717 this.command_manager.reset(); 31718 const _layers = this.layer_manager.get_layers(); 31719 _layers.forEach((l) => this.layer_manager.delete_layer(l.id)); 31720 if (layer_urls === void 0 || layer_urls.length === 0) { 31721 this.layer_manager.create_layer({ 31722 width: this.width, 31723 height: this.height, 31724 layer_name: void 0, 31725 user_created: false 31726 }); 31727 return; 31728 } 31729 const created_layer_ids = []; 31730 for await (const url of layer_urls) { 31731 const layer_id = await this.layer_manager.add_layer_from_url(url); 31732 if (layer_id) { 31733 created_layer_ids.push(layer_id); 31734 } 31735 } 31736 if (created_layer_ids.length > 0) { 31737 this.layer_manager.set_active_layer(created_layer_ids[0]); 31738 } 31739 const resize_tool = this.tools.get("resize"); 31740 if (resize_tool && typeof resize_tool.set_border_region === "function") { 31741 resize_tool.set_border_region(this.border_region); 31742 } 31743 this.notify("change"); 31744 this.notify("input"); 31745 } 31746 set_layer(id) { 31747 this.layer_manager.set_active_layer(id); 31748 this.notify("change"); 31749 } 31750 move_layer(id, direction) { 31751 const reorder_layer_command = new ReorderLayerCommand( 31752 this.context, 31753 id, 31754 direction 31755 ); 31756 this.execute_command(reorder_layer_command); 31757 this.notify("change"); 31758 } 31759 delete_layer(id) { 31760 const remove_layer_command = new RemoveLayerCommand(this.context, id); 31761 this.execute_command(remove_layer_command); 31762 this.notify("change"); 31763 } 31764 modify_canvas_size(width, height, anchor, scale) { 31765 const oldWidth = this.width; 31766 const oldHeight = this.height; 31767 this.layer_manager.resize_all_layers( 31768 width, 31769 height, 31770 scale, 31771 anchor, 31772 oldWidth, 31773 oldHeight 31774 ); 31775 this.width = width; 31776 this.height = height; 31777 this.set_image_properties({ 31778 width, 31779 height, 31780 scale: 1, 31781 position: { x: 0, y: 0 }, 31782 animate: false 31783 }); 31784 this.notify("change"); 31785 } 31786 async get_blobs() { 31787 const blobs = await this.layer_manager.get_blobs(this.width, this.height); 31788 return blobs; 31789 } 31790 on(event, callback) { 31791 this.event_callbacks.set(event, [ 31792 ...this.event_callbacks.get(event) || [], 31793 callback 31794 ]); 31795 } 31796 off(event, callback) { 31797 this.event_callbacks.set( 31798 event, 31799 this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || [] 31800 ); 31801 } 31802 notify(event) { 31803 for (const callback of this.event_callbacks.get(event) || []) { 31804 callback(); 31805 } 31806 } 31807 destroy() { 31808 if (!this.app) return; 31809 this.app?.destroy(); 31810 } 31811 resize(width, height) { 31812 this.set_image_properties({ 31813 width, 31814 height 31815 }); 31816 this.reset(); 31817 } 31818 async get_crop_bounds() { 31819 const crop_tool = this.tools.get("crop"); 31820 const crop_bounds = crop_tool.get_crop_bounds(); 31821 const image = await crop_tool.get_image(); 31822 return { 31823 image, 31824 ...crop_bounds 31825 }; 31826 } 31827 get background_image_sprite() { 31828 return this.background_image; 31829 } 31830 set_layer_options(layer_options) { 31831 this.layer_options = layer_options; 31832 this.layer_manager.set_layer_options( 31833 layer_options, 31834 this.width, 31835 this.height 31836 ); 31837 } 31838 toggle_layer_visibility(id) { 31839 this.layer_manager.toggle_layer_visibility(id); 31840 } 31841} 31842 31843class CropTool { 31844 name = "crop"; 31845 image_editor_context; 31846 current_tool = "image"; 31847 current_subtool = "crop"; 31848 // Constants 31849 CORNER_SIZE = 25; 31850 LINE_THICKNESS = 5; 31851 HANDLE_COLOR = 0; 31852 HIT_AREA_SIZE = 40; 31853 // State 31854 is_dragging = false; 31855 selected_handle = null; 31856 active_corner_index = -1; 31857 active_edge_index = -1; 31858 last_pointer_position = null; 31859 crop_bounds = { x: 0, y: 0, width: 0, height: 0 }; 31860 crop_ui_container = null; 31861 crop_mask = null; 31862 dimensions = { 31863 width: 0, 31864 height: 0 31865 }; 31866 position = { x: 0, y: 0 }; 31867 scale = 1; 31868 is_dragging_window = false;
31869 drag_start_position = null; 31870 drag_start_bounds = null; 31871 background_image_watcher = null; 31872 has_been_manually_changed = false; 31873 event_callbacks = /* @__PURE__ */ new Map(); 31874 /** 31875 * @method setup 31876 * @async 31877 * @description Sets up the crop tool with the given context and state 31878 * @param {ImageEditorContext} context - The image editor context 31879 * @param {ToolbarTool} tool - The current tool 31880 * @param {Subtool} subtool - The current subtool 31881 * @returns {Promise<void>} 31882 */ 31883 async setup(context, tool, subtool) { 31884 this.image_editor_context = context; 31885 this.current_tool = tool; 31886 this.current_subtool = subtool; 31887 context.dimensions.subscribe((dimensions) => { 31888 this.dimensions = dimensions; 31889 this.crop_bounds = { 31890 x: 0, 31891 y: 0, 31892 width: dimensions.width, 31893 height: dimensions.height 31894 }; 31895 if (this.crop_ui_container) { 31896 this.update_crop_ui(); 31897 } 31898 this.set_crop_mask(); 31899 this.update_crop_mask(); 31900 }); 31901 context.position.subscribe((position) => { 31902 this.position = position; 31903 }); 31904 context.scale.subscribe((scale) => { 31905 this.scale = scale; 31906 }); 31907 this.background_image_watcher = () => { 31908 if (this.crop_mask && this.current_tool === "image" && this.current_subtool === "crop") { 31909 if (this.image_editor_context.background_image) { 31910 this.image_editor_context.background_image.setMask(this.crop_mask); 31911 } 31912 } 31913 }; 31914 this.image_editor_context.app.ticker.add(this.background_image_watcher); 31915 await this.init_crop_ui(); 31916 this.setup_event_listeners(); 31917 const isCropActive = tool === "image" && subtool === "crop"; 31918 if (this.crop_ui_container) { 31919 this.crop_ui_container.visible = isCropActive; 31920 } 31921 } 31922 /** 31923 * @method cleanup 31924 * @description Cleans up the crop tool resources and removes event listeners 31925 * @returns {void} 31926 */ 31927 cleanup() { 31928 if (this.crop_ui_container) { 31929 this.image_editor_context.app.stage.removeChild(this.crop_ui_container); 31930 } 31931 if (this.crop_mask) { 31932 if (this.crop_mask.parent) { 31933 this.crop_mask.parent.removeChild(this.crop_mask); 31934 } 31935 if (this.image_editor_context.background_image && this.image_editor_context.background_image.mask === this.crop_mask) { 31936 this.image_editor_context.background_image.mask = null; 31937 } 31938 if (this.image_editor_context.image_container.mask === this.crop_mask) { 31939 this.image_editor_context.image_container.mask = null; 31940 } 31941 } 31942 this.crop_mask = null; 31943 this.crop_ui_container = null; 31944 if (this.background_image_watcher) { 31945 this.image_editor_context.app.ticker.remove( 31946 this.background_image_watcher 31947 ); 31948 this.background_image_watcher = null; 31949 } 31950 this.cleanup_event_listeners(); 31951 } 31952 /** 31953 * @method set_tool 31954 * @description Sets the current tool and subtool, updating UI visibility 31955 * @param {ToolbarTool} tool - The tool to set 31956 * @param {Subtool} subtool - The subtool to set 31957 * @returns {void} 31958 */ 31959 set_tool(tool, subtool) { 31960 this.current_tool = tool; 31961 this.current_subtool = subtool; 31962 const isCropActive = tool === "image" && subtool === "crop"; 31963 if (this.crop_ui_container) { 31964 this.crop_ui_container.visible = isCropActive; 31965 } 31966 if (isCropActive && this.crop_mask) { 31967 this.update_crop_mask(); 31968 } 31969 } 31970 /** 31971 * @method set_crop_mask 31972 * @private 31973 * @description Sets up the crop mask for the image 31974 * @returns {void} 31975 */ 31976 set_crop_mask() { 31977 if (this.crop_mask) { 31978 if (this.crop_mask.parent) { 31979 this.crop_mask.parent.removeChild(this.crop_mask); 31980 } 31981 if (this.image_editor_context.image_container.mask === this.crop_mask) { 31982 this.image_editor_context.image_container.mask = null; 31983 } 31984 if (this.image_editor_context.background_image && this.image_editor_context.background_image.mask === this.crop_mask) { 31985 this.image_editor_context.background_image.mask = null; 31986 } 31987 } 31988 this.crop_mask = new Graphics(); 31989 this.image_editor_context.image_container.addChild(this.crop_mask); 31990 } 31991 /** 31992 * @method init_crop_ui 31993 * @private 31994 * @async 31995 * @description Initializes the crop UI elements and sets up the update loop 31996 * @returns {Promise<void>} 31997 */
31998 async init_crop_ui() { 31999 const { width, height } = this.image_editor_context.background_image?.getLocalBounds() || { 32000 width: false, 32001 height: false 32002 }; 32003 this.crop_ui_container = this.make_crop_ui( 32004 this.dimensions.width * this.scale, 32005 this.dimensions.height * this.scale 32006 ); 32007 this.crop_ui_container.position.set(this.position.x, this.position.y); 32008 this.crop_ui_container.visible = false; 32009 this.image_editor_context.app.stage.addChild(this.crop_ui_container); 32010 this.set_crop_mask(); 32011 this.image_editor_context.app.ticker.add(this.update_crop_ui.bind(this)); 32012 this.update_crop_mask(); 32013 } 32014 /** 32015 * @method make_crop_ui 32016 * @private 32017 * @description Creates the crop UI with the specified dimensions 32018 * @param {number} width - Width of the crop area 32019 * @param {number} height - Height of the crop area 32020 * @returns {Container} The created crop UI container 32021 */ 32022 make_crop_ui(width, height) { 32023 const crop_container = new Container(); 32024 crop_container.eventMode = "static"; 32025 crop_container.interactiveChildren = true; 32026 const crop_outline = new Graphics().rect(0, 0, width, height).stroke({ width: 1, color: 0, alignment: 0, alpha: 0.5 }); 32027 crop_outline.eventMode = "static"; 32028 crop_outline.cursor = "move"; 32029 crop_outline.hitArea = new Rectangle(0, 0, width, height); 32030 crop_outline.on("pointerdown", this.handle_window_drag_start.bind(this)); 32031 crop_container.addChild(crop_outline); 32032 this.create_corner_handles(crop_container, width, height); 32033 this.create_edge_handles(crop_container, width, height); 32034 return crop_container; 32035 } 32036 /** 32037 * @method create_handle 32038 * @private 32039 * @description Creates a handle for the crop UI 32040 * @param {boolean} [is_edge=false] - Whether the handle is an edge handle 32041 * @returns {Container} The created handle container 32042 */ 32043 create_handle(is_edge = false) { 32044 const handle = new Container(); 32045 handle.eventMode = "static"; 32046 const handle_graphics = new Graphics(); 32047 if (is_edge) { 32048 handle_graphics.rect(0, 0, this.CORNER_SIZE * 1.5, this.LINE_THICKNESS).fill(this.HANDLE_COLOR); 32049 } else { 32050 handle_graphics.rect(0, 0, this.CORNER_SIZE, this.LINE_THICKNESS).rect(0, 0, this.LINE_THICKNESS, this.CORNER_SIZE).fill(this.HANDLE_COLOR); 32051 } 32052 handle.addChild(handle_graphics); 32053 const hit_size = is_edge ? this.HIT_AREA_SIZE * 1.5 : this.HIT_AREA_SIZE; 32054 handle.hitArea = new Rectangle( 32055 -hit_size / 2 + this.LINE_THICKNESS, 32056 -hit_size / 2 + this.LINE_THICKNESS, 32057 hit_size, 32058 hit_size 32059 ); 32060 return handle; 32061 } 32062 /** 32063 * @method create_edge_handles 32064 * @private 32065 * @description Creates edge handles for the crop UI 32066 * @param {Container} container - The container to add handles to 32067 * @param {number} width - Width of the crop area 32068 * @param {number} height - Height of the crop area 32069 * @returns {void} 32070 */ 32071 create_edge_handles(container, width, height) { 32072 [-1, 1].forEach((i, index) => { 32073 const handle = this.create_handle(true); 32074 handle.rotation = 0; 32075 handle.on("pointerdown", (event) => { 32076 this.handle_pointer_down(event, handle, -1, index); 32077 }); 32078 handle.x = width / 2 - this.CORNER_SIZE * 3 / 2; 32079 handle.y = i < 0 ? -this.LINE_THICKNESS : height; 32080 handle.cursor = "ns-resize"; 32081 container.addChild(handle); 32082 }); 32083 [-1, 1].forEach((i, index) => { 32084 const handle = this.create_handle(true); 32085 handle.rotation = Math.PI / 2; 32086 handle.on("pointerdown", (event) => { 32087 this.handle_pointer_down(event, handle, -1, index + 2); 32088 }); 32089 handle.x = i < 0 ? -this.LINE_THICKNESS : width; 32090 handle.y = height / 2 - this.CORNER_SIZE * 3 / 2; 32091 handle.cursor = "ew-resize"; 32092 container.addChild(handle); 32093 }); 32094 } 32095 /** 32096 * @method create_corner_handles 32097 * @private 32098 * @description Creates corner handles for the crop UI 32099 * @param {Container} container - The container to add handles to 32100 * @param {number} width - Width of the crop area 32101 * @param {number} height - Height of the crop area 32102 * @returns {void} 32103 */ 32104 create_corner_handles(container, width, height) { 32105 const corners = [ 32106 { x: 0, y: 0, xScale: 1, yScale: 1, cursor: "nwse-resize" }, 32107 // Top-left 32108 { x: width, y: 0, xScale: -1, yScale: 1, cursor: "nesw-resize" }, 32109 // Top-right 32110 { x: 0, y: height, xScale: 1, yScale: -1, cursor: "nesw-resize" }, 32111 // Bottom-left 32112 { x: width, y: height, xScale: -1, yScale: -1, cursor: "nwse-resize" } 32113 // Bottom-right 32114 ];
32115 corners.forEach(({ x, y, xScale, yScale, cursor }, i) => { 32116 const corner = this.create_handle(false); 32117 corner.x = x - (xScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS); 32118 corner.y = y - (yScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS); 32119 corner.scale.set(xScale, yScale); 32120 corner.on("pointerdown", (event) => { 32121 this.handle_pointer_down(event, corner, i, -1); 32122 }); 32123 corner.cursor = cursor; 32124 container.addChild(corner); 32125 }); 32126 } 32127 /** 32128 * @method handle_pointer_down 32129 * @private 32130 * @description Handles pointer down events on handles 32131 * @param {FederatedPointerEvent} event - The pointer event 32132 * @param {Container} handle - The handle that was clicked 32133 * @param {number} corner_index - Index of the corner (-1 if not a corner) 32134 * @param {number} edge_index - Index of the edge (-1 if not an edge) 32135 * @returns {void} 32136 */ 32137 handle_pointer_down(event, handle, corner_index, edge_index) { 32138 if (this.current_subtool !== "crop") return; 32139 event.stopPropagation(); 32140 this.is_dragging = true; 32141 this.selected_handle = handle; 32142 this.active_corner_index = corner_index; 32143 this.active_edge_index = edge_index; 32144 const local_pos = this.image_editor_context.image_container.toLocal( 32145 event.global 32146 ); 32147 this.last_pointer_position = new Point(local_pos.x, local_pos.y); 32148 } 32149 /** 32150 * updates the crop bounds based on pointer movement 32151 * @param delta - the change in position 32152 */ 32153 update_crop_bounds(delta) { 32154 this.has_been_manually_changed = true; 32155 const scaled_delta = new Point(delta.x, delta.y); 32156 const original = { 32157 x: this.crop_bounds.x, 32158 y: this.crop_bounds.y, 32159 width: this.crop_bounds.width, 32160 height: this.crop_bounds.height 32161 }; 32162 if (this.active_corner_index !== -1) { 32163 switch (this.active_corner_index) { 32164 case 0: 32165 const newWidth0 = Math.max(20, original.width - scaled_delta.x); 32166 const widthDiff0 = original.width - newWidth0; 32167 this.crop_bounds.width = newWidth0; 32168 this.crop_bounds.x = original.x + widthDiff0; 32169 const newHeight0 = Math.max(20, original.height - scaled_delta.y); 32170 const heightDiff0 = original.height - newHeight0; 32171 this.crop_bounds.height = newHeight0; 32172 this.crop_bounds.y = original.y + heightDiff0; 32173 break; 32174 case 1: 32175 this.crop_bounds.width = Math.max( 32176 20, 32177 original.width + scaled_delta.x 32178 ); 32179 const newHeight1 = Math.max(20, original.height - scaled_delta.y); 32180 const heightDiff1 = original.height - newHeight1; 32181 this.crop_bounds.height = newHeight1; 32182 this.crop_bounds.y = original.y + heightDiff1; 32183 break; 32184 case 2: 32185 const newWidth2 = Math.max(20, original.width - scaled_delta.x); 32186 const widthDiff2 = original.width - newWidth2; 32187 this.crop_bounds.width = newWidth2; 32188 this.crop_bounds.x = original.x + widthDiff2; 32189 this.crop_bounds.height = Math.max( 32190 20, 32191 original.height + scaled_delta.y 32192 ); 32193 break; 32194 case 3: 32195 this.crop_bounds.width = Math.max( 32196 20, 32197 original.width + scaled_delta.x 32198 ); 32199 this.crop_bounds.height = Math.max( 32200 20, 32201 original.height + scaled_delta.y 32202 ); 32203 break; 32204 } 32205 } else if (this.active_edge_index !== -1) { 32206 switch (this.active_edge_index) { 32207 case 0: 32208 const newHeight = Math.max(20, original.height - scaled_delta.y); 32209 const heightDiff = original.height - newHeight; 32210 this.crop_bounds.height = newHeight; 32211 this.crop_bounds.y = original.y + heightDiff; 32212 break; 32213 case 1: 32214 this.crop_bounds.height = Math.max( 32215 20, 32216 original.height + scaled_delta.y 32217 ); 32218 break; 32219 case 2: 32220 const newWidth = Math.max(20, original.width - scaled_delta.x); 32221 const widthDiff = original.width - newWidth; 32222 this.crop_bounds.width = newWidth; 32223 this.crop_bounds.x = original.x + widthDiff; 32224 break; 32225 case 3: 32226 this.crop_bounds.width = Math.max( 32227 20, 32228 original.width + scaled_delta.x 32229 ); 32230 break; 32231 } 32232 } 32233 this.constrain_crop_bounds(); 32234 this.update_crop_ui(); 32235 this.update_crop_mask(); 32236 if (this.crop_mask && this.image_editor_context.background_image) { 32237 this.image_editor_context.background_image.setMask(this.crop_mask); 32238 } 32239 } 32240 /** 32241 * constrains the crop bounds to the image dimensions 32242 */ 32243 constrain_crop_bounds() { 32244 ({ 32245 x: this.crop_bounds.x, 32246 y: this.crop_bounds.y, 32247 width: this.crop_bounds.width, 32248 height: this.crop_bounds.height 32249 }); 32250 this.crop_bounds.width = Math.max( 32251 20, 32252 Math.min(this.crop_bounds.width, this.dimensions.width) 32253 ); 32254 this.crop_bounds.height = Math.max( 32255 20, 32256 Math.min(this.crop_bounds.height, this.dimensions.height) 32257 ); 32258 const oldX = this.crop_bounds.x; 32259 this.crop_bounds.x = Math.max( 32260 0, 32261 Math.min( 32262 this.crop_bounds.x, 32263 this.dimensions.width - this.crop_bounds.width 32264 ) 32265 ); 32266 if (this.crop_bounds.x !== oldX && this.active_corner_index === 0 || this.active_edge_index === 2) { 32267 const xDiff = this.crop_bounds.x - oldX; 32268 this.crop_bounds.width -= xDiff; 32269 } 32270 const oldY = this.crop_bounds.y; 32271 this.crop_bounds.y = Math.max( 32272 0, 32273 Math.min( 32274 this.crop_bounds.y, 32275 this.dimensions.height - this.crop_bounds.height 32276 ) 32277 ); 32278 if (this.crop_bounds.y !== oldY && (this.active_corner_index === 0 || this.active_corner_index === 1 || this.active_edge_index === 0)) { 32279 const yDiff = this.crop_bounds.y - oldY; 32280 this.crop_bounds.height -= yDiff; 32281 } 32282 this.crop_bounds.width = Math.max( 32283 20, 32284 Math.min( 32285 this.crop_bounds.width, 32286 this.dimensions.width - this.crop_bounds.x 32287 ) 32288 ); 32289 this.crop_bounds.height = Math.max( 32290 20, 32291 Math.min( 32292 this.crop_bounds.height, 32293 this.dimensions.height - this.crop_bounds.y 32294 ) 32295 ); 32296 } 32297 /** 32298 * updates the crop mask graphics 32299 */ 32300 update_crop_mask() { 32301 if (!this.crop_mask) return; 32302 this.crop_mask.clear(); 32303 const { width, height } = this.image_editor_context.background_image?.getLocalBounds() ?? { 32304 width: 0, 32305 height: 0 32306 }; 32307 this.crop_mask.rect(0, 0, width, height).fill({ color: 0, alpha: 0.4 }).rect( 32308 this.crop_bounds.x, 32309 this.crop_bounds.y, 32310 this.crop_bounds.width, 32311 this.crop_bounds.height 32312 ).cut(); 32313 if (this.image_editor_context.background_image) { 32314 this.image_editor_context.background_image.mask = null; 32315 }
32316 if (this.image_editor_context.image_container.mask === this.crop_mask) { 32317 this.image_editor_context.image_container.mask = null; 32318 } 32319 } 32320 /** 32321 * updates the crop ui position and dimensions 32322 */ 32323 update_crop_ui() { 32324 if (!this.crop_mask || !this.crop_ui_container) return; 32325 this.crop_ui_container.position.set( 32326 this.position.x + this.crop_bounds.x * this.scale, 32327 this.position.y + this.crop_bounds.y * this.scale 32328 ); 32329 const scaled_width = this.crop_bounds.width * this.scale; 32330 const scaled_height = this.crop_bounds.height * this.scale; 32331 const outline = this.crop_ui_container.getChildAt(0); 32332 outline.clear().rect(0, 0, scaled_width, scaled_height).stroke({ width: 1, color: 0, alignment: 0, alpha: 0.5 }); 32333 outline.hitArea = new Rectangle(0, 0, scaled_width, scaled_height); 32334 this.update_handle_positions(scaled_width, scaled_height); 32335 } 32336 /** 32337 * updates the positions of all handles 32338 * @param width - width of the crop area 32339 * @param height - height of the crop area 32340 */ 32341 update_handle_positions(width, height) { 32342 if (!this.crop_ui_container) return; 32343 const handles = this.crop_ui_container.children.slice(1); 32344 const corners = handles.slice(0, 4); 32345 const cornerPositions = [ 32346 { x: 0, y: 0 }, 32347 // Top-left 32348 { x: width, y: 0 }, 32349 // Top-right 32350 { x: 0, y: height }, 32351 // Bottom-left 32352 { x: width, y: height } 32353 // Bottom-right 32354 ]; 32355 corners.forEach((handle, i) => { 32356 const xScale = i % 2 === 0 ? 1 : -1; 32357 const yScale = i < 2 ? 1 : -1; 32358 handle.position.set( 32359 cornerPositions[i].x - (xScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS), 32360 cornerPositions[i].y - (yScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS) 32361 ); 32362 }); 32363 const edges = handles.slice(4);
32364 edges.forEach((handle, i) => { 32365 if (i < 2) { 32366 handle.position.set( 32367 width / 2 - this.CORNER_SIZE * 1.5 / 2, 32368 // Changed from 3 to 2 32369 i === 0 ? -this.LINE_THICKNESS : height 32370 ); 32371 } else { 32372 handle.position.set( 32373 i === 2 ? 0 : width + this.LINE_THICKNESS, 32374 height / 2 - this.CORNER_SIZE * 1.5 / 2 32375 // Changed from 3 to 2 32376 ); 32377 } 32378 }); 32379 } 32380 /** 32381 * sets up event listeners for the crop tool 32382 */ 32383 setup_event_listeners() { 32384 const stage = this.image_editor_context.app.stage; 32385 stage.eventMode = "static"; 32386 stage.on("pointermove", this.handle_pointer_move.bind(this)); 32387 stage.on("pointerup", this.handle_pointer_up.bind(this)); 32388 stage.on("pointerupoutside", this.handle_pointer_up.bind(this)); 32389 } 32390 /** 32391 * removes event listeners for the crop tool 32392 */ 32393 cleanup_event_listeners() { 32394 const stage = this.image_editor_context.app.stage; 32395 stage.off("pointermove", this.handle_pointer_move.bind(this)); 32396 stage.off("pointerup", this.handle_pointer_up.bind(this)); 32397 stage.off("pointerupoutside", this.handle_pointer_up.bind(this)); 32398 } 32399 /** 32400 * handles pointer move events 32401 * @param event - the pointer event 32402 */ 32403 handle_pointer_move(event) { 32404 if (this.current_subtool !== "crop") return; 32405 if (this.is_dragging_window && this.drag_start_position && this.drag_start_bounds) { 32406 const local_pos = this.image_editor_context.image_container.toLocal( 32407 event.global 32408 ); 32409 const delta = new Point( 32410 local_pos.x - this.drag_start_position.x, 32411 local_pos.y - this.drag_start_position.y 32412 ); 32413 this.crop_bounds = { 32414 x: this.drag_start_bounds.x + delta.x, 32415 y: this.drag_start_bounds.y + delta.y, 32416 width: this.drag_start_bounds.width, 32417 height: this.drag_start_bounds.height 32418 }; 32419 this.constrain_crop_bounds(); 32420 this.update_crop_mask(); 32421 if (this.crop_mask && this.image_editor_context.background_image) { 32422 this.image_editor_context.background_image.setMask(this.crop_mask); 32423 } 32424 return; 32425 } 32426 if (this.is_dragging && this.selected_handle && this.last_pointer_position) { 32427 const local_pos = this.image_editor_context.image_container.toLocal( 32428 event.global 32429 ); 32430 const current_position = new Point(local_pos.x, local_pos.y); 32431 const delta = new Point( 32432 current_position.x - this.last_pointer_position.x, 32433 current_position.y - this.last_pointer_position.y 32434 ); 32435 this.update_crop_bounds(delta); 32436 this.last_pointer_position = current_position; 32437 this.update_crop_mask(); 32438 } 32439 } 32440 /** 32441 * handles pointer up events 32442 */ 32443 handle_pointer_up() { 32444 if (this.current_subtool !== "crop") return; 32445 this.is_dragging = false; 32446 this.is_dragging_window = false; 32447 this.selected_handle = null; 32448 this.last_pointer_position = null; 32449 this.drag_start_position = null; 32450 this.drag_start_bounds = null; 32451 this.active_corner_index = -1; 32452 this.active_edge_index = -1; 32453 this.notify("change"); 32454 } 32455 /** 32456 * handles the start of window dragging 32457 * @param event - the pointer event 32458 */ 32459 handle_window_drag_start(event) { 32460 if (this.current_subtool !== "crop") return; 32461 event.stopPropagation(); 32462 this.is_dragging_window = true; 32463 this.has_been_manually_changed = true; 32464 const local_pos = this.image_editor_context.image_container.toLocal( 32465 event.global 32466 ); 32467 this.drag_start_position = new Point(local_pos.x, local_pos.y); 32468 this.drag_start_bounds = { ...this.crop_bounds }; 32469 } 32470 /** 32471 * @returns whether the crop has been manually changed by the user 32472 */ 32473 get crop_manually_changed() { 32474 return this.has_been_manually_changed; 32475 } 32476 on(event, callback) { 32477 this.event_callbacks.set(event, [ 32478 ...this.event_callbacks.get(event) || [], 32479 callback 32480 ]); 32481 } 32482 off(event, callback) { 32483 this.event_callbacks.set( 32484 event, 32485 this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || [] 32486 ); 32487 } 32488 notify(event) { 32489 for (const callback of this.event_callbacks.get(event) || []) { 32490 callback(); 32491 } 32492 } 32493 get_crop_bounds() { 32494 const { width, height } = this.image_editor_context.background_image?.getLocalBounds() ?? { 32495 width: 0, 32496 height: 0 32497 }; 32498 return { 32499 x: this.crop_bounds.x, 32500 y: this.crop_bounds.y, 32501 crop_dimensions: { 32502 width: this.crop_bounds.width, 32503 height: this.crop_bounds.height 32504 }, 32505 image_dimensions: { 32506 width, 32507 height 32508 } 32509 }; 32510 } 32511 get_image() { 32512 if (!this.image_editor_context.background_image) 32513 return Promise.resolve(null); 32514 const container = new Container(); 32515 const sprite = new Sprite( 32516 this.image_editor_context.background_image.texture 32517 ); 32518 container.addChild(sprite); 32519 return get_canvas_blob( 32520 this.image_editor_context.app.renderer, 32521 container, 32522 this.crop_bounds 32523 ); 32524 } 32525} 32526 32527class ResizeCommand { 32528 constructor(context, original_state, new_state, reset_ui) { 32529 this.context = context; 32530 this.name = "Resize"; 32531 this.original_width = original_state.width; 32532 this.original_height = original_state.height; 32533 this.original_x = original_state.x; 32534 this.original_y = original_state.y; 32535 this.new_width = new_state.width; 32536 this.new_height = new_state.height;
32537 this.new_x = new_state.x; 32538 this.new_y = new_state.y; 32539 this.reset_ui = reset_ui; 32540 } 32541 original_width; 32542 original_height; 32543 original_x; 32544 original_y; 32545 new_width; 32546 new_height; 32547 new_x; 32548 new_y; 32549 reset_ui; 32550 name; 32551 async execute(context) { 32552 if (context) { 32553 this.context = context; 32554 } 32555 if (!this.context.background_image) return; 32556 this.context.background_image.width = this.new_width; 32557 this.context.background_image.height = this.new_height; 32558 this.context.background_image.position.set(this.new_x, this.new_y); 32559 this.reset_ui(); 32560 } 32561 async undo() { 32562 if (!this.context.background_image) return; 32563 this.context.background_image.width = this.original_width; 32564 this.context.background_image.height = this.original_height; 32565 this.context.background_image.position.set( 32566 this.original_x, 32567 this.original_y 32568 ); 32569 this.reset_ui(); 32570 } 32571} 32572class ResizeTool { 32573 name = "resize"; 32574 image_editor_context; 32575 current_tool = "image"; 32576 current_subtool = "size"; 32577 current_cursor = "unset"; 32578 CORNER_SIZE = 25; 32579 LINE_THICKNESS = 5; 32580 HANDLE_COLOR = 0; 32581 HIT_AREA_SIZE = 40; 32582 borderRegion = 0; 32583 is_dragging = false; 32584 selected_handle = null; 32585 active_corner_index = -1; 32586 active_edge_index = -1; 32587 last_pointer_position = null; 32588 resize_ui_container = null; 32589 dimensions = { 32590 width: 0, 32591 height: 0 32592 }; 32593 position = { x: 0, y: 0 }; 32594 scale = 1; 32595 is_moving = false; 32596 dom_mousedown_handler = null;
32597 dom_mousemove_handler = null; 32598 dom_mouseup_handler = null; 32599 event_callbacks = /* @__PURE__ */ new Map(); 32600 last_scale = 1; 32601 original_state = null; 32602 /** 32603 * @method setup 32604 * @async 32605 * @description Sets up the resize tool with the given context and tool/subtool 32606 * @param {ImageEditorContext} context - The image editor context 32607 * @param {ToolbarTool} tool - The current tool 32608 * @param {Subtool} subtool - The current subtool 32609 * @returns {Promise<void>} 32610 */ 32611 async setup(context, tool, subtool) { 32612 this.image_editor_context = context; 32613 this.current_tool = tool; 32614 this.current_subtool = subtool; 32615 if (context.background_image) { 32616 if (context.background_image.borderRegion !== void 0) { 32617 this.borderRegion = context.background_image.borderRegion; 32618 } else { 32619 const bg = context.background_image; 32620 const container = context.image_container; 32621 const isHorizontallyCentered = Math.abs(bg.position.x - (container.width - bg.width) / 2) < 2; 32622 const isVerticallyCentered = Math.abs(bg.position.y - (container.height - bg.height) / 2) < 2; 32623 if (!isHorizontallyCentered || !isVerticallyCentered) { 32624 this.borderRegion = Math.max(bg.position.x, bg.position.y); 32625 context.background_image.borderRegion = this.borderRegion; 32626 } 32627 } 32628 } 32629 context.dimensions.subscribe((dimensions) => { 32630 this.dimensions = dimensions; 32631 if (this.resize_ui_container) { 32632 this.update_resize_ui(); 32633 } 32634 }); 32635 context.position.subscribe((position) => { 32636 this.position = position; 32637 if (this.resize_ui_container && this.current_tool === "image" && this.current_subtool === "size") { 32638 this.update_resize_ui(); 32639 } 32640 }); 32641 context.scale.subscribe((scale) => { 32642 this.scale = scale; 32643 if (this.resize_ui_container && this.current_tool === "image" && this.current_subtool === "size") { 32644 this.update_resize_ui(); 32645 } 32646 }); 32647 await this.init_resize_ui(); 32648 this.setup_event_listeners(); 32649 if (this.current_subtool === "size" && this.image_editor_context.background_image) { 32650 const bg = this.image_editor_context.background_image; 32651 const container = this.image_editor_context.image_container; 32652 const bg_width = bg.width; 32653 const bg_height = bg.height; 32654 const container_bounds = container.getLocalBounds(); 32655 const container_width = container_bounds.width; 32656 const container_height = container_bounds.height; 32657 const min_x = -(bg_width - container_width); 32658 const min_y = -(bg_height - container_height); 32659 const max_x = 0; 32660 const max_y = 0; 32661 const x_out_of_bounds = bg_width > container_width && (bg.position.x < min_x || bg.position.x > max_x); 32662 const y_out_of_bounds = bg_height > container_height && (bg.position.y < min_y || bg.position.y > max_y); 32663 if (x_out_of_bounds || y_out_of_bounds) { 32664 this.apply_boundary_constraints(); 32665 } 32666 } 32667 } 32668 /** 32669 * @method cleanup 32670 * @description Cleans up the resize tool resources and removes event listeners 32671 * @returns {void} 32672 */ 32673 cleanup() { 32674 if (this.resize_ui_container) { 32675 this.image_editor_context.app.stage.removeChild(this.resize_ui_container); 32676 } 32677 this.cleanup_event_listeners(); 32678 } 32679 /** 32680 * @method set_tool 32681 * @description Sets the current tool and subtool, updating UI visibility 32682 * @param {ToolbarTool} tool - The tool to set 32683 * @param {Subtool} subtool - The subtool to set 32684 * @returns {void} 32685 */ 32686 set_tool(tool, subtool) { 32687 this.current_tool = tool; 32688 this.current_subtool = subtool; 32689 if (tool === "image" && subtool === "size") { 32690 if (this.image_editor_context.background_image) { 32691 const storedBorderRegion = this.image_editor_context.background_image.borderRegion; 32692 if (typeof storedBorderRegion === "number" && storedBorderRegion > 0) { 32693 this.borderRegion = storedBorderRegion; 32694 } 32695 } 32696 this.show_resize_ui(); 32697 if (this.image_editor_context.background_image) { 32698 const bg = this.image_editor_context.background_image;
32699 const container = this.image_editor_context.image_container; 32700 const bg_width = bg.width; 32701 const bg_height = bg.height; 32702 const container_bounds = container.getLocalBounds(); 32703 const container_width = container_bounds.width; 32704 const container_height = container_bounds.height; 32705 const effectiveWidth = container_width - this.borderRegion * 2; 32706 const effectiveHeight = container_height - this.borderRegion * 2; 32707 const min_x = bg_width > effectiveWidth ? -(bg_width - effectiveWidth) + this.borderRegion : this.borderRegion; 32708 const max_x = bg_width > effectiveWidth ? this.borderRegion : container_width - bg_width - this.borderRegion; 32709 const min_y = bg_height > effectiveHeight ? -(bg_height - effectiveHeight) + this.borderRegion : this.borderRegion; 32710 const max_y = bg_height > effectiveHeight ? this.borderRegion : container_height - bg_height - this.borderRegion; 32711 const x_out_of_bounds = bg.position.x < min_x || bg.position.x > max_x; 32712 const y_out_of_bounds = bg.position.y < min_y || bg.position.y > max_y; 32713 if (x_out_of_bounds || y_out_of_bounds) { 32714 this.apply_boundary_constraints(); 32715 } 32716 } 32717 } else { 32718 this.hide_resize_ui(); 32719 } 32720 } 32721 /** 32722 * @method init_resize_ui 32723 * @private 32724 * @async 32725 * @description Initializes the resize UI elements and sets up the update loop 32726 * @returns {Promise<void>} 32727 */ 32728 async init_resize_ui() { 32729 this.resize_ui_container = this.make_resize_ui( 32730 this.dimensions.width * this.scale, 32731 this.dimensions.height * this.scale 32732 ); 32733 this.resize_ui_container.position.set(this.position.x, this.position.y); 32734 this.resize_ui_container.visible = false; 32735 this.image_editor_context.app.stage.addChild(this.resize_ui_container); 32736 } 32737 /** 32738 * @method make_resize_ui 32739 * @private 32740 * @description Creates the resize UI with the specified dimensions 32741 * @param {number} width - Width of the resize area 32742 * @param {number} height - Height of the resize area 32743 * @returns {Container} The created resize UI container 32744 */ 32745 make_resize_ui(width, height) { 32746 const resize_container = new Container(); 32747 resize_container.eventMode = "static"; 32748 resize_container.interactiveChildren = true; 32749 const outline = new Graphics().rect(0, 0, width, height).stroke({ 32750 width: 1, 32751 color: 0, 32752 alignment: 0, 32753 alpha: 0.3 32754 }); 32755 const dotted_outline = new Graphics(); 32756 dotted_outline.rect(0, 0, width, height).stroke({ 32757 width: 1, 32758 color: 0, 32759 alpha: 0.7, 32760 pixelLine: true 32761 }); 32762 const dash_length = 5; 32763 const gap_length = 5; 32764 const total_length = dash_length + gap_length; 32765 for (let x = 0; x < width; x += total_length * 2) { 32766 dotted_outline.rect(x, 0, Math.min(dash_length, width - x), 1).fill(0); 32767 dotted_outline.rect(x, height - 1, Math.min(dash_length, width - x), 1).fill(0); 32768 } 32769 for (let y = 0; y < height; y += total_length * 2) { 32770 dotted_outline.rect(0, y, 1, Math.min(dash_length, height - y)).fill(0); 32771 dotted_outline.rect(width - 1, y, 1, Math.min(dash_length, height - y)).fill(0); 32772 } 32773 resize_container.addChild(outline); 32774 resize_container.addChild(dotted_outline); 32775 const move_area = new Graphics().rect(0, 0, width, height).fill(16777215, 0); 32776 move_area.eventMode = "static"; 32777 move_area.cursor = "move"; 32778 resize_container.addChild(move_area); 32779 this.create_corner_handles(resize_container, width, height); 32780 this.create_edge_handles(resize_container, width, height); 32781 return resize_container; 32782 } 32783 /** 32784 * @method create_handle 32785 * @private 32786 * @description Creates a handle for the resize UI 32787 * @param {boolean} [is_edge=false] - Whether the handle is an edge handle 32788 * @returns {Container} The created handle container 32789 */ 32790 create_handle(is_edge = false) { 32791 const handle = new Container(); 32792 handle.eventMode = "static"; 32793 handle.cursor = "pointer"; 32794 const handle_graphics = new Graphics(); 32795 const handle_size = is_edge ? 8 : 10; 32796 handle_graphics.rect(-handle_size / 2, -handle_size / 2, handle_size, handle_size).fill(16777215).stroke({ width: 1, color: this.HANDLE_COLOR }); 32797 handle.addChild(handle_graphics); 32798 const hit_size = is_edge ? this.HIT_AREA_SIZE * 1.5 : this.HIT_AREA_SIZE; 32799 handle.hitArea = new Rectangle( 32800 -hit_size / 2, 32801 -hit_size / 2, 32802 hit_size, 32803 hit_size 32804 ); 32805 return handle; 32806 } 32807 /** 32808 * @method create_edge_handles 32809 * @private 32810 * @description Creates edge handles for the resize UI 32811 * @param {Container} container - The container to add handles to 32812 * @param {number} width - Width of the resize area 32813 * @param {number} height - Height of the resize area 32814 * @returns {void} 32815 */ 32816 create_edge_handles(container, width, height) {
32817 [-1, 1].forEach((i, index) => { 32818 const handle = this.create_handle(true); 32819 handle.rotation = 0; 32820 handle.on("pointerdown", (event) => { 32821 this.handle_pointer_down(event, handle, -1, index); 32822 }); 32823 handle.x = width / 2; 32824 handle.y = i < 0 ? 0 : height; 32825 handle.cursor = "ns-resize"; 32826 container.addChild(handle); 32827 }); 32828 [-1, 1].forEach((i, index) => { 32829 const handle = this.create_handle(true); 32830 handle.rotation = 0; 32831 handle.on("pointerdown", (event) => { 32832 this.handle_pointer_down(event, handle, -1, index + 2); 32833 }); 32834 handle.x = i < 0 ? 0 : width; 32835 handle.y = height / 2; 32836 handle.cursor = "ew-resize"; 32837 container.addChild(handle); 32838 }); 32839 } 32840 /** 32841 * @method create_corner_handles 32842 * @private 32843 * @description Creates corner handles for the resize UI 32844 * @param {Container} container - The container to add handles to 32845 * @param {number} width - Width of the resize area 32846 * @param {number} height - Height of the resize area 32847 * @returns {void} 32848 */ 32849 create_corner_handles(container, width, height) { 32850 const corners = [ 32851 { x: 0, y: 0, cursor: "nwse-resize" }, 32852 { x: width, y: 0, cursor: "nesw-resize" }, 32853 { x: 0, y: height, cursor: "nesw-resize" }, 32854 { x: width, y: height, cursor: "nwse-resize" } 32855 ]; 32856 corners.forEach(({ x, y, cursor }, i) => { 32857 const corner = this.create_handle(false); 32858 corner.x = x; 32859 corner.y = y; 32860 corner.on("pointerdown", (event) => { 32861 this.handle_pointer_down(event, corner, i, -1); 32862 }); 32863 corner.cursor = cursor; 32864 container.addChild(corner); 32865 }); 32866 } 32867 /** 32868 * @method handle_pointer_down 32869 * @private 32870 * @description Handles pointer down events on handles 32871 * @param {FederatedPointerEvent} event - The pointer event 32872 * @param {Container} handle - The handle that was clicked 32873 * @param {number} corner_index - Index of the corner (-1 if not a corner) 32874 * @param {number} edge_index - Index of the edge (-1 if not an edge) 32875 * @returns {void} 32876 */ 32877 handle_pointer_down(event, handle, corner_index, edge_index) { 32878 if (this.current_subtool !== "size") return; 32879 event.stopPropagation(); 32880 this.is_dragging = true; 32881 this.selected_handle = handle; 32882 this.active_corner_index = corner_index; 32883 this.active_edge_index = edge_index; 32884 const local_pos = this.image_editor_context.image_container.toLocal( 32885 event.global 32886 ); 32887 this.last_pointer_position = new Point(local_pos.x, local_pos.y); 32888 if (this.image_editor_context.background_image) { 32889 const bg = this.image_editor_context.background_image; 32890 this.original_state = { 32891 width: bg.width, 32892 height: bg.height, 32893 x: bg.position.x, 32894 y: bg.position.y 32895 }; 32896 } 32897 } 32898 /** 32899 * Maintains aspect ratio during resizing 32900 * @param new_width - The new width 32901 * @param new_height - The new height 32902 * @param original_aspect_ratio - The original aspect ratio 32903 * @param delta - The delta to apply 32904 * @returns The adjusted dimensions 32905 */ 32906 maintain_aspect_ratio(new_width, new_height, original_aspect_ratio, delta) { 32907 const abs_delta_x = Math.abs(delta.x); 32908 const abs_delta_y = Math.abs(delta.y); 32909 if (abs_delta_x > abs_delta_y * 1.2) { 32910 new_height = new_width / original_aspect_ratio; 32911 } else if (abs_delta_y > abs_delta_x * 1.2) { 32912 new_width = new_height * original_aspect_ratio; 32913 } else { 32914 if (new_width / original_aspect_ratio > new_height) { 32915 new_height = new_width / original_aspect_ratio; 32916 } else { 32917 new_width = new_height * original_aspect_ratio; 32918 } 32919 } 32920 return { width: new_width, height: new_height }; 32921 } 32922 /** 32923 * Limits dimensions based on constraints 32924 * @param width - The width to limit 32925 * @param height - The height to limit 32926 * @param maintain_aspect_ratio - Whether to maintain aspect ratio 32927 * @returns The limited dimensions 32928 */ 32929 limit_dimensions(width, height, maintain_aspect_ratio) { 32930 const max_width = this.dimensions.width; 32931 const max_height = this.dimensions.height; 32932 let new_width = width; 32933 let new_height = height; 32934 if (new_width > max_width) { 32935 if (maintain_aspect_ratio && this.active_corner_index !== -1) { 32936 new_height = max_width / new_width * new_height; 32937 } 32938 new_width = max_width; 32939 } 32940 if (new_height > max_height) { 32941 if (maintain_aspect_ratio && this.active_corner_index !== -1) { 32942 new_width = max_height / new_height * new_width; 32943 } 32944 new_height = max_height; 32945 } 32946 new_width = Math.max(20, new_width); 32947 new_height = Math.max(20, new_height); 32948 return { width: new_width, height: new_height }; 32949 } 32950 /** 32951 * Calculates position deltas based on dimension changes 32952 * @param old_width - The original width 32953 * @param old_height - The original height 32954 * @param new_width - The new width 32955 * @param new_height - The new height 32956 * @returns Object containing x and y position deltas 32957 */ 32958 calculate_position_deltas(old_width, old_height, new_width, new_height) { 32959 let delta_x = 0; 32960 let delta_y = 0; 32961 if (this.active_corner_index === 0 || this.active_edge_index === 2) { 32962 delta_x = old_width - new_width; 32963 } else if (this.active_corner_index === 2) { 32964 delta_x = old_width - new_width; 32965 delta_y = 0; 32966 } 32967 if (this.active_corner_index === 0 || this.active_corner_index === 1 || this.active_edge_index === 0) { 32968 delta_y = old_height - new_height; 32969 } 32970 return { x: delta_x, y: delta_y }; 32971 } 32972 /** 32973 * Applies boundary constraints to keep the image within the container 32974 * @private 32975 */ 32976 apply_boundary_constraints() { 32977 if (!this.image_editor_context.background_image) return; 32978 const bg = this.image_editor_context.background_image;
32979 const image_container = this.image_editor_context.image_container; 32980 let new_x = bg.position.x; 32981 let new_y = bg.position.y; 32982 const container_width = image_container.width; 32983 const container_height = image_container.height; 32984 const bg_width = bg.width; 32985 const bg_height = bg.height; 32986 const effectiveWidth = container_width - this.borderRegion * 2; 32987 const effectiveHeight = container_height - this.borderRegion * 2; 32988 if (bg_width > effectiveWidth) { 32989 const min_x = -(bg_width - effectiveWidth) + this.borderRegion; 32990 const max_x = this.borderRegion; 32991 new_x = Math.max(min_x, Math.min(max_x, new_x)); 32992 } else { 32993 const min_x = this.borderRegion; 32994 const max_x = container_width - bg_width - this.borderRegion; 32995 if (new_x < min_x || new_x > max_x) { 32996 new_x = Math.max(min_x, Math.min(max_x, new_x)); 32997 } 32998 } 32999 if (bg_height > effectiveHeight) { 33000 const min_y = -(bg_height - effectiveHeight) + this.borderRegion; 33001 const max_y = this.borderRegion; 33002 new_y = Math.max(min_y, Math.min(max_y, new_y)); 33003 } else { 33004 const min_y = this.borderRegion; 33005 const max_y = container_height - bg_height - this.borderRegion; 33006 if (new_y < min_y || new_y > max_y) { 33007 new_y = Math.max(min_y, Math.min(max_y, new_y)); 33008 } 33009 } 33010 bg.position.set(new_x, new_y); 33011 if (this.resize_ui_container) { 33012 const total_x = image_container.position.x + new_x * this.scale; 33013 const total_y = image_container.position.y + new_y * this.scale; 33014 this.resize_ui_container.position.set(total_x, total_y); 33015 } 33016 } 33017 /** 33018 * Updates the resize bounds based on the delta 33019 * @param delta - The delta to apply 33020 * @param maintain_aspect_ratio - Whether to maintain the aspect ratio 33021 */ 33022 update_resize_bounds(delta, maintain_aspect_ratio = false) { 33023 const background_image = this.image_editor_context.background_image; 33024 if (!background_image) return; 33025 const original_width = background_image.width; 33026 const original_height = background_image.height; 33027 const original_x = background_image.position.x; 33028 const original_y = background_image.position.y; 33029 const original_aspect_ratio = original_width / original_height; 33030 let current_position; 33031 if (this.active_corner_index !== -1) { 33032 switch (this.active_corner_index) { 33033 case 0: 33034 current_position = new Point( 33035 original_x - delta.x, 33036 original_y - delta.y 33037 ); 33038 break; 33039 case 1: 33040 current_position = new Point( 33041 original_x + original_width + delta.x, 33042 original_y - delta.y 33043 ); 33044 break; 33045 case 2: 33046 current_position = new Point( 33047 original_x - delta.x, 33048 original_y + original_height + delta.y 33049 ); 33050 break; 33051 case 3: 33052 current_position = new Point( 33053 original_x + original_width + delta.x, 33054 original_y + original_height + delta.y 33055 ); 33056 break; 33057 default: 33058 return; 33059 } 33060 const dimensions = this.handle_direct_corner_resize( 33061 current_position, 33062 original_width, 33063 original_height, 33064 original_x, 33065 original_y, 33066 original_aspect_ratio, 33067 maintain_aspect_ratio 33068 ); 33069 background_image.width = dimensions.width; 33070 background_image.height = dimensions.height; 33071 background_image.position.set(dimensions.x, dimensions.y); 33072 } else if (this.active_edge_index !== -1) { 33073 switch (this.active_edge_index) { 33074 case 0: 33075 current_position = new Point( 33076 original_x + original_width / 2, 33077 original_y - delta.y 33078 ); 33079 break; 33080 case 1: 33081 current_position = new Point( 33082 original_x + original_width / 2, 33083 original_y + original_height + delta.y 33084 ); 33085 break; 33086 case 2: 33087 current_position = new Point( 33088 original_x - delta.x, 33089 original_y + original_height / 2 33090 ); 33091 break; 33092 case 3: 33093 current_position = new Point( 33094 original_x + original_width + delta.x, 33095 original_y + original_height / 2 33096 ); 33097 break; 33098 default: 33099 return; 33100 } 33101 const dimensions = this.handle_direct_edge_resize( 33102 current_position, 33103 original_width, 33104 original_height, 33105 original_x, 33106 original_y 33107 ); 33108 background_image.width = dimensions.width; 33109 background_image.height = dimensions.height; 33110 background_image.position.set(dimensions.x, dimensions.y); 33111 } 33112 this.update_resize_ui(); 33113 } 33114 /** 33115 * updates the resize ui position and dimensions 33116 */ 33117 update_resize_ui() { 33118 if (!this.resize_ui_container || !this.image_editor_context.background_image || !this.image_editor_context.background_image.position) 33119 return; 33120 const background_image = this.image_editor_context.background_image;
33121 const image_container = this.image_editor_context.image_container; 33122 const container_global_pos = image_container.getGlobalPosition(); 33123 const bg_pos_x = background_image.position.x * this.scale; 33124 const bg_pos_y = background_image.position.y * this.scale; 33125 this.resize_ui_container.position.set( 33126 container_global_pos.x + bg_pos_x, 33127 container_global_pos.y + bg_pos_y 33128 ); 33129 const scaled_width = background_image.width * this.scale; 33130 const scaled_height = background_image.height * this.scale; 33131 const outline = this.resize_ui_container.getChildAt(0); 33132 outline.clear().rect(0, 0, scaled_width, scaled_height).stroke({ width: 1, color: 0, alignment: 0, alpha: 0.3 }); 33133 const dotted_outline = this.resize_ui_container.getChildAt(1); 33134 dotted_outline.clear(); 33135 dotted_outline.rect(0, 0, scaled_width, scaled_height).stroke({ 33136 width: 1, 33137 color: 0, 33138 alpha: 0.7, 33139 pixelLine: true 33140 }); 33141 if (!this.is_dragging && !this.is_moving && Math.abs(this.scale - this.last_scale) < 1e-3) { 33142 const dash_length = 5; 33143 const gap_length = 5; 33144 const total_length = dash_length + gap_length; 33145 for (let x = 0; x < scaled_width; x += total_length * 2) { 33146 dotted_outline.rect(x, 0, Math.min(dash_length, scaled_width - x), 1).fill(0); 33147 dotted_outline.rect( 33148 x, 33149 scaled_height - 1, 33150 Math.min(dash_length, scaled_width - x), 33151 1 33152 ).fill(0); 33153 } 33154 for (let y = 0; y < scaled_height; y += total_length * 2) { 33155 dotted_outline.rect(0, y, 1, Math.min(dash_length, scaled_height - y)).fill(0); 33156 dotted_outline.rect( 33157 scaled_width - 1, 33158 y, 33159 1, 33160 Math.min(dash_length, scaled_height - y) 33161 ).fill(0); 33162 } 33163 } 33164 this.last_scale = this.scale; 33165 const move_area = this.resize_ui_container.getChildAt(2); 33166 move_area.clear().rect(0, 0, scaled_width, scaled_height).fill(16777215, 0); 33167 this.update_handle_positions(scaled_width, scaled_height); 33168 } 33169 /** 33170 * updates the positions of all handles 33171 * @param width - width of the resize area 33172 * @param height - height of the resize area 33173 */ 33174 update_handle_positions(width, height) { 33175 if (!this.resize_ui_container) return; 33176 const handles = this.resize_ui_container.children.slice(3); 33177 const move_area = this.resize_ui_container.getChildAt(2); 33178 move_area.clear().rect(0, 0, width, height).fill(16777215, 0); 33179 const corners = handles.slice(0, 4); 33180 const cornerPositions = [ 33181 { x: 0, y: 0 }, 33182 { x: width, y: 0 }, 33183 { x: 0, y: height }, 33184 { x: width, y: height } 33185 ]; 33186 corners.forEach((handle, i) => { 33187 handle.position.set(cornerPositions[i].x, cornerPositions[i].y); 33188 }); 33189 const edges = handles.slice(4);
33190 edges.forEach((handle, i) => { 33191 if (i < 2) { 33192 handle.position.set(width / 2, i === 0 ? 0 : height); 33193 } else { 33194 handle.position.set(i === 2 ? 0 : width, height / 2); 33195 } 33196 }); 33197 } 33198 /** 33199 * sets up event listeners for the resize tool 33200 */ 33201 setup_event_listeners() { 33202 const stage = this.image_editor_context.app.stage; 33203 stage.eventMode = "static"; 33204 stage.on("pointermove", this.handle_pointer_move.bind(this)); 33205 stage.on("pointerup", this.handle_pointer_up.bind(this)); 33206 stage.on("pointerupoutside", this.handle_pointer_up.bind(this)); 33207 this.setup_dom_event_listeners(); 33208 } 33209 /** 33210 * Sets up direct DOM event listeners for dragging the background image 33211 */ 33212 setup_dom_event_listeners() { 33213 this.cleanup_dom_event_listeners(); 33214 const canvas = this.image_editor_context.app.canvas; 33215 this.dom_mousedown_handler = (e) => { 33216 if (this.current_subtool !== "size") return; 33217 if (this.is_dragging || this.selected_handle) return; 33218 if (!this.image_editor_context.background_image) return; 33219 const bg = this.image_editor_context.background_image; 33220 const container = this.image_editor_context.image_container; 33221 const containerBounds = container.getBounds(); 33222 const rect = canvas.getBoundingClientRect(); 33223 const x = e.clientX - rect.left; 33224 const y = e.clientY - rect.top; 33225 const localX = (x - containerBounds.x) / this.scale; 33226 const localY = (y - containerBounds.y) / this.scale; 33227 if (localX >= bg.position.x && localX <= bg.position.x + bg.width && localY >= bg.position.y && localY <= bg.position.y + bg.height) { 33228 this.is_moving = true; 33229 this.last_pointer_position = new Point(localX, localY); 33230 this.original_state = { 33231 width: bg.width, 33232 height: bg.height, 33233 x: bg.position.x, 33234 y: bg.position.y 33235 }; 33236 e.preventDefault(); 33237 e.stopPropagation(); 33238 } 33239 }; 33240 this.dom_mousemove_handler = (e) => { 33241 if (this.current_subtool !== "size" || !this.is_moving) return; 33242 if (!this.last_pointer_position || !this.image_editor_context.background_image) 33243 return; 33244 const container = this.image_editor_context.image_container; 33245 const containerBounds = container.getBounds(); 33246 const rect = canvas.getBoundingClientRect(); 33247 const x = e.clientX - rect.left; 33248 const y = e.clientY - rect.top; 33249 const localX = (x - containerBounds.x) / this.scale; 33250 const localY = (y - containerBounds.y) / this.scale; 33251 const current_position = new Point(localX, localY); 33252 this.handle_image_dragging(current_position); 33253 this.last_pointer_position = current_position; 33254 e.preventDefault(); 33255 e.stopPropagation(); 33256 }; 33257 this.dom_mouseup_handler = (e) => { 33258 if (this.current_subtool !== "size") return; 33259 if (this.original_state && this.image_editor_context.background_image) { 33260 const bg = this.image_editor_context.background_image; 33261 const new_state = { 33262 width: bg.width, 33263 height: bg.height, 33264 x: bg.position.x, 33265 y: bg.position.y 33266 }; 33267 if (this.original_state.width !== new_state.width || this.original_state.height !== new_state.height || this.original_state.x !== new_state.x || this.original_state.y !== new_state.y) { 33268 const resize_command = new ResizeCommand( 33269 this.image_editor_context, 33270 this.original_state, 33271 new_state, 33272 this.update_resize_ui.bind(this) 33273 ); 33274 this.image_editor_context.execute_command(resize_command); 33275 } 33276 this.original_state = null; 33277 } 33278 this.is_moving = false; 33279 this.last_pointer_position = null; 33280 this.update_resize_ui(); 33281 e.preventDefault(); 33282 e.stopPropagation(); 33283 }; 33284 canvas.addEventListener("mousedown", this.dom_mousedown_handler); 33285 window.addEventListener("mousemove", this.dom_mousemove_handler); 33286 window.addEventListener("mouseup", this.dom_mouseup_handler); 33287 } 33288 /** 33289 * Cleans up DOM event listeners 33290 */ 33291 cleanup_dom_event_listeners() { 33292 const canvas = this.image_editor_context.app.canvas;
33293 if (this.dom_mousedown_handler) { 33294 canvas.removeEventListener("mousedown", this.dom_mousedown_handler); 33295 this.dom_mousedown_handler = null; 33296 } 33297 if (this.dom_mousemove_handler) { 33298 window.removeEventListener("mousemove", this.dom_mousemove_handler); 33299 this.dom_mousemove_handler = null; 33300 } 33301 if (this.dom_mouseup_handler) { 33302 window.removeEventListener("mouseup", this.dom_mouseup_handler); 33303 this.dom_mouseup_handler = null; 33304 } 33305 } 33306 /** 33307 * removes event listeners for the resize tool 33308 */ 33309 cleanup_event_listeners() { 33310 const stage = this.image_editor_context.app.stage; 33311 stage.off("pointermove", this.handle_pointer_move.bind(this)); 33312 stage.off("pointerup", this.handle_pointer_up.bind(this)); 33313 stage.off("pointerupoutside", this.handle_pointer_up.bind(this)); 33314 this.cleanup_dom_event_listeners(); 33315 } 33316 /** 33317 * Handles pointer move events 33318 */ 33319 handle_pointer_move(event) { 33320 if (this.current_subtool !== "size") return; 33321 const local_pos = this.image_editor_context.image_container.toLocal( 33322 event.global 33323 ); 33324 const current_position = new Point(local_pos.x, local_pos.y); 33325 if (this.is_moving && this.last_pointer_position) { 33326 this.handle_image_dragging(current_position); 33327 this.last_pointer_position = current_position; 33328 } else if (this.is_dragging && this.selected_handle) { 33329 if (!this.image_editor_context.background_image) return; 33330 const bg = this.image_editor_context.background_image; 33331 const maintain_aspect_ratio = this.active_corner_index !== -1 && !event.shiftKey; 33332 const original_width = bg.width; 33333 const original_height = bg.height; 33334 const original_x = bg.position.x; 33335 const original_y = bg.position.y; 33336 const original_aspect_ratio = original_width / original_height; 33337 let dimensions; 33338 if (this.active_corner_index !== -1) { 33339 dimensions = this.handle_direct_corner_resize( 33340 current_position, 33341 original_width, 33342 original_height, 33343 original_x, 33344 original_y, 33345 original_aspect_ratio, 33346 maintain_aspect_ratio 33347 ); 33348 } else if (this.active_edge_index !== -1) { 33349 dimensions = this.handle_direct_edge_resize( 33350 current_position, 33351 original_width, 33352 original_height, 33353 original_x, 33354 original_y 33355 ); 33356 } else { 33357 return; 33358 } 33359 dimensions = this.limit_dimensions_to_container( 33360 dimensions, 33361 original_x, 33362 original_y, 33363 original_width, 33364 original_height, 33365 maintain_aspect_ratio 33366 ); 33367 bg.width = dimensions.width; 33368 bg.height = dimensions.height; 33369 bg.position.set(dimensions.x, dimensions.y); 33370 this.update_resize_ui(); 33371 this.last_pointer_position = current_position; 33372 } 33373 } 33374 /** 33375 * handles pointer up events 33376 */ 33377 handle_pointer_up() { 33378 if (this.current_subtool !== "size") return; 33379 this.is_dragging = false; 33380 this.is_moving = false; 33381 this.selected_handle = null; 33382 this.last_pointer_position = null; 33383 this.active_corner_index = -1; 33384 this.active_edge_index = -1; 33385 this.update_resize_ui(); 33386 this.notify("change"); 33387 } 33388 /** 33389 * Handles dragging of the background image 33390 * @param current_position - The current pointer position 33391 * @private 33392 */ 33393 handle_image_dragging(current_position) { 33394 if (!this.last_pointer_position || !this.image_editor_context.background_image) { 33395 return; 33396 } 33397 const bg = this.image_editor_context.background_image; 33398 const image_container = this.image_editor_context.image_container.getLocalBounds(); 33399 const dx = current_position.x - this.last_pointer_position.x; 33400 const dy = current_position.y - this.last_pointer_position.y; 33401 let new_x = bg.position.x + dx; 33402 let new_y = bg.position.y + dy; 33403 const container_width = image_container.width; 33404 const container_height = image_container.height; 33405 const bg_width = bg.width; 33406 const bg_height = bg.height; 33407 const effectiveWidth = container_width - this.borderRegion * 2; 33408 const effectiveHeight = container_height - this.borderRegion * 2; 33409 if (bg_width > effectiveWidth) { 33410 const min_x = -(bg_width - effectiveWidth) + this.borderRegion; 33411 const max_x = this.borderRegion; 33412 new_x = Math.max(min_x, Math.min(max_x, new_x)); 33413 } else { 33414 const min_x = this.borderRegion; 33415 const max_x = container_width - bg_width - this.borderRegion; 33416 new_x = Math.max(min_x, Math.min(max_x, new_x)); 33417 } 33418 if (bg_height > effectiveHeight) { 33419 const min_y = -(bg_height - effectiveHeight) + this.borderRegion;
33420 const max_y = this.borderRegion; 33421 new_y = Math.max(min_y, Math.min(max_y, new_y)); 33422 } else { 33423 const min_y = this.borderRegion; 33424 const max_y = container_height - bg_height - this.borderRegion; 33425 new_y = Math.max(min_y, Math.min(max_y, new_y)); 33426 } 33427 bg.position.set(new_x, new_y); 33428 this.update_resize_ui(); 33429 } 33430 /** 33431 * Handles corner resizing 33432 * @param current_position - The current mouse position 33433 * @param original_width - The original width 33434 * @param original_height - The original height 33435 * @param original_x - The original x position 33436 * @param original_y - The original y position 33437 * @param original_aspect_ratio - The original aspect ratio 33438 * @param maintain_aspect_ratio - Whether to maintain aspect ratio 33439 * @returns The new dimensions and position 33440 * @private 33441 */ 33442 handle_direct_corner_resize(current_position, original_width, original_height, original_x, original_y, original_aspect_ratio, maintain_aspect_ratio) { 33443 let raw_width = 0; 33444 let raw_height = 0; 33445 let new_x = original_x; 33446 let new_y = original_y; 33447 switch (this.active_corner_index) { 33448 case 0: 33449 raw_width = original_x + original_width - current_position.x; 33450 raw_height = original_y + original_height - current_position.y; 33451 break; 33452 case 1: 33453 raw_width = current_position.x - original_x; 33454 raw_height = original_y + original_height - current_position.y; 33455 break; 33456 case 2: 33457 raw_width = original_x + original_width - current_position.x; 33458 raw_height = current_position.y - original_y; 33459 break; 33460 case 3: 33461 raw_width = current_position.x - original_x; 33462 raw_height = current_position.y - original_y; 33463 break; 33464 } 33465 raw_width = Math.max(20, raw_width); 33466 raw_height = Math.max(20, raw_height); 33467 let new_width = raw_width; 33468 let new_height = raw_height; 33469 if (maintain_aspect_ratio) { 33470 const diagonal_vector = { 33471 x: current_position.x - (original_x + original_width / 2), 33472 y: current_position.y - (original_y + original_height / 2) 33473 }; 33474 const angle = Math.abs(Math.atan2(diagonal_vector.y, diagonal_vector.x)); 33475 if (angle < Math.PI / 4 || angle > 3 * Math.PI / 4) { 33476 new_height = new_width / original_aspect_ratio; 33477 } else { 33478 new_width = new_height * original_aspect_ratio; 33479 } 33480 } 33481 switch (this.active_corner_index) { 33482 case 0: 33483 new_x = original_x + original_width - new_width; 33484 new_y = original_y + original_height - new_height; 33485 break; 33486 case 1: 33487 new_y = original_y + original_height - new_height; 33488 break; 33489 case 2: 33490 new_x = original_x + original_width - new_width; 33491 break; 33492 } 33493 return { width: new_width, height: new_height, x: new_x, y: new_y }; 33494 } 33495 /** 33496 * Handles edge resizing 33497 * @param current_position - The current mouse position 33498 * @param original_width - The original width 33499 * @param original_height - The original height 33500 * @param original_x - The original x position 33501 * @param original_y - The original y position 33502 * @returns The new dimensions and position 33503 * @private 33504 */ 33505 handle_direct_edge_resize(current_position, original_width, original_height, original_x, original_y) { 33506 let new_width = original_width; 33507 let new_height = original_height; 33508 let new_x = original_x; 33509 let new_y = original_y; 33510 switch (this.active_edge_index) { 33511 case 0: 33512 new_height = Math.max( 33513 20, 33514 original_y + original_height - current_position.y 33515 ); 33516 new_y = current_position.y; 33517 break; 33518 case 1: 33519 new_height = Math.max(20, current_position.y - original_y); 33520 break; 33521 case 2: 33522 new_width = Math.max( 33523 20, 33524 original_x + original_width - current_position.x 33525 ); 33526 new_x = current_position.x; 33527 break; 33528 case 3: 33529 new_width = Math.max(20, current_position.x - original_x); 33530 break; 33531 } 33532 return { width: new_width, height: new_height, x: new_x, y: new_y }; 33533 } 33534 /** 33535 * Limits dimensions to container size 33536 * @param dimensions - The dimensions to limit 33537 * @param original_x - The original x position 33538 * @param original_y - The original y position 33539 * @param original_width - The original width 33540 * @param original_height - The original height 33541 * @param maintain_aspect_ratio - Whether to maintain aspect ratio 33542 * @returns The limited dimensions and position 33543 * @private 33544 */ 33545 limit_dimensions_to_container(dimensions, original_x, original_y, original_width, original_height, maintain_aspect_ratio) { 33546 let { 33547 width: new_width, 33548 height: new_height, 33549 x: new_x, 33550 y: new_y 33551 } = dimensions; 33552 const effective_container_width = this.dimensions.width - this.borderRegion * 2; 33553 const effective_container_height = this.dimensions.height - this.borderRegion * 2; 33554 const pre_limit_width = new_width; 33555 const pre_limit_height = new_height; 33556 const size_limited = this.apply_size_limits( 33557 new_width, 33558 new_height, 33559 effective_container_width, 33560 effective_container_height, 33561 maintain_aspect_ratio, 33562 pre_limit_width / pre_limit_height 33563 ); 33564 new_width = size_limited.width; 33565 new_height = size_limited.height; 33566 const position_adjusted = this.adjust_position_for_resizing( 33567 new_width, 33568 new_height, 33569 original_x, 33570 original_y, 33571 original_width, 33572 original_height 33573 ); 33574 new_x = position_adjusted.x; 33575 new_y = position_adjusted.y; 33576 const border_constrained = this.apply_border_constraints( 33577 new_width, 33578 new_height, 33579 new_x, 33580 new_y, 33581 maintain_aspect_ratio, 33582 pre_limit_width / pre_limit_height 33583 ); 33584 return border_constrained; 33585 } 33586 /** 33587 * Apply size limits to ensure dimensions don't exceed container 33588 * @private 33589 */ 33590 apply_size_limits(width, height, max_width, max_height, maintain_aspect_ratio, aspect_ratio) { 33591 let new_width = width; 33592 let new_height = height; 33593 const needs_width_limit = new_width > max_width; 33594 const needs_height_limit = new_height > max_height; 33595 if (needs_width_limit && needs_height_limit) { 33596 const width_scale = max_width / new_width; 33597 const height_scale = max_height / new_height; 33598 if (width_scale < height_scale) { 33599 new_width = max_width; 33600 if (maintain_aspect_ratio) { 33601 new_height = new_width / aspect_ratio; 33602 } 33603 } else { 33604 new_height = max_height; 33605 if (maintain_aspect_ratio) { 33606 new_width = new_height * aspect_ratio; 33607 } 33608 } 33609 } else if (needs_width_limit) { 33610 new_width = max_width; 33611 if (maintain_aspect_ratio) { 33612 new_height = new_width / aspect_ratio; 33613 } 33614 } else if (needs_height_limit) { 33615 new_height = max_height; 33616 if (maintain_aspect_ratio) { 33617 new_width = new_height * aspect_ratio; 33618 } 33619 } 33620 return { width: new_width, height: new_height }; 33621 } 33622 /** 33623 * Adjust position based on which handles are being dragged 33624 * @private 33625 */ 33626 adjust_position_for_resizing(new_width, new_height, original_x, original_y, original_width, original_height) {
33627 let new_x = original_x; 33628 let new_y = original_y; 33629 switch (this.active_corner_index) { 33630 case 0: 33631 new_x = original_x + original_width - new_width; 33632 new_y = original_y + original_height - new_height; 33633 break; 33634 case 1: 33635 new_y = original_y + original_height - new_height; 33636 break; 33637 case 2: 33638 new_x = original_x + original_width - new_width; 33639 break; 33640 } 33641 if (this.active_edge_index !== -1) { 33642 switch (this.active_edge_index) { 33643 case 0: 33644 new_y = original_y + original_height - new_height; 33645 break; 33646 case 2: 33647 new_x = original_x + original_width - new_width; 33648 break; 33649 } 33650 } 33651 return { x: new_x, y: new_y }; 33652 } 33653 /** 33654 * Apply border region constraints to prevent image from intruding on border 33655 * @private 33656 */ 33657 apply_border_constraints(width, height, x, y, maintain_aspect_ratio, aspect_ratio) { 33658 const top_left_constrained = this.apply_top_left_constraints( 33659 width, 33660 height, 33661 x, 33662 y, 33663 maintain_aspect_ratio, 33664 aspect_ratio 33665 ); 33666 const all_constrained = this.apply_bottom_right_constraints( 33667 top_left_constrained.width, 33668 top_left_constrained.height, 33669 top_left_constrained.x, 33670 top_left_constrained.y, 33671 maintain_aspect_ratio, 33672 aspect_ratio 33673 ); 33674 all_constrained.width = Math.max(20, all_constrained.width); 33675 all_constrained.height = Math.max(20, all_constrained.height); 33676 return all_constrained; 33677 } 33678 /** 33679 * Apply top and left border constraints 33680 * @private 33681 */ 33682 apply_top_left_constraints(width, height, x, y, maintain_aspect_ratio, aspect_ratio) { 33683 let new_width = width; 33684 let new_height = height; 33685 let new_x = x; 33686 let new_y = y; 33687 if (new_x < this.borderRegion) { 33688 if (this.active_corner_index === 0 || this.active_corner_index === 2 || this.active_edge_index === 2) { 33689 new_width -= this.borderRegion - new_x; 33690 if (maintain_aspect_ratio) { 33691 new_height = new_width / aspect_ratio; 33692 } 33693 } 33694 new_x = this.borderRegion; 33695 } 33696 if (new_y < this.borderRegion) { 33697 if (this.active_corner_index === 0 || this.active_corner_index === 1 || this.active_edge_index === 0) { 33698 new_height -= this.borderRegion - new_y; 33699 if (maintain_aspect_ratio) { 33700 new_width = new_height * aspect_ratio; 33701 } 33702 } 33703 new_y = this.borderRegion; 33704 } 33705 return { width: new_width, height: new_height, x: new_x, y: new_y }; 33706 } 33707 /** 33708 * Apply bottom and right border constraints 33709 * @private 33710 */ 33711 apply_bottom_right_constraints(width, height, x, y, maintain_aspect_ratio, aspect_ratio) { 33712 let new_width = width; 33713 let new_height = height; 33714 let new_x = x; 33715 let new_y = y; 33716 if (new_x + new_width > this.dimensions.width - this.borderRegion) { 33717 if (this.active_corner_index === 1 || this.active_corner_index === 3 || this.active_edge_index === 3) { 33718 new_width = this.dimensions.width - this.borderRegion - new_x; 33719 if (maintain_aspect_ratio) { 33720 new_height = new_width / aspect_ratio; 33721 } 33722 } else { 33723 const right_edge = new_x + new_width; 33724 const excess = right_edge - (this.dimensions.width - this.borderRegion); 33725 new_x = Math.max(this.borderRegion, new_x - excess); 33726 } 33727 } 33728 if (new_y + new_height > this.dimensions.height - this.borderRegion) { 33729 if (this.active_corner_index === 2 || this.active_corner_index === 3 || this.active_edge_index === 1) { 33730 new_height = this.dimensions.height - this.borderRegion - new_y; 33731 if (maintain_aspect_ratio) { 33732 new_width = new_height * aspect_ratio; 33733 } 33734 } else { 33735 const bottom_edge = new_y + new_height; 33736 const excess = bottom_edge - (this.dimensions.height - this.borderRegion); 33737 new_y = Math.max(this.borderRegion, new_y - excess); 33738 } 33739 } 33740 return { width: new_width, height: new_height, x: new_x, y: new_y }; 33741 } 33742 /** 33743 * Shows the resize UI 33744 * @private 33745 */ 33746 show_resize_ui() { 33747 if (this.resize_ui_container) { 33748 this.resize_ui_container.visible = true; 33749 this.update_resize_ui(); 33750 } 33751 if (this.image_editor_context.background_image) { 33752 const bg = this.image_editor_context.background_image;
33753 this.current_cursor = bg.cursor; 33754 bg.cursor = "move"; 33755 } 33756 this.setup_dom_event_listeners(); 33757 } 33758 /** 33759 * Hides the resize UI 33760 * @private 33761 */ 33762 hide_resize_ui() { 33763 if (this.resize_ui_container) { 33764 this.resize_ui_container.visible = false; 33765 } 33766 if (this.image_editor_context.background_image) { 33767 const bg = this.image_editor_context.background_image; 33768 bg.cursor = this.current_cursor; 33769 } 33770 this.cleanup_dom_event_listeners(); 33771 } 33772 on(event, callback) { 33773 this.event_callbacks.set(event, [ 33774 ...this.event_callbacks.get(event) || [], 33775 callback 33776 ]); 33777 } 33778 off(event, callback) { 33779 this.event_callbacks.set( 33780 event, 33781 this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || [] 33782 ); 33783 } 33784 notify(event) { 33785 for (const callback of this.event_callbacks.get(event) || []) { 33786 callback(); 33787 } 33788 } 33789 /** 33790 * Sets the border region explicitly 33791 * @param borderRegion The border region size in pixels 33792 */ 33793 set_border_region(borderRegion) { 33794 this.borderRegion = borderRegion; 33795 if (this.image_editor_context?.background_image) { 33796 this.image_editor_context.background_image.borderRegion = borderRegion; 33797 } 33798 if (this.resize_ui_container) { 33799 this.update_resize_ui(); 33800 } 33801 if (this.current_subtool === "size" && this.image_editor_context?.background_image) { 33802 this.apply_boundary_constraints(); 33803 } 33804 } 33805} 33806 33807function set_cursor(child, cursor) { 33808 if (child instanceof Container) { 33809 child.cursor = cursor; 33810 } else if ("cursor" in child) { 33811 child.cursor = cursor; 33812 } 33813} 33814function recurse_set_cursor(children, cursor) { 33815 for (const child of children) { 33816 set_cursor(child, cursor); 33817 if (child instanceof Container && child.children.length > 0) { 33818 recurse_set_cursor( 33819 child.children, 33820 cursor 33821 ); 33822 } 33823 } 33824} 33825function clear_timeout(timeout) { 33826 if (timeout !== null) { 33827 window.clearTimeout(timeout); 33828 } 33829 return null; 33830} 33831 33832class BrushCursor { 33833 constructor(image_editor_context, state, scale) { 33834 this.image_editor_context = image_editor_context; 33835 this.state = state; 33836 this.scale = scale; 33837 this.initialize_cursor(); 33838 this.initialize_brush_preview(); 33839 } 33840 cursor_graphics = null; 33841 cursor_container = null; 33842 brush_preview_container = null; 33843 brush_preview_graphics = null; 33844 is_preview_visible = false; 33845 is_cursor_over_image = false; 33846 cursor_position_check_timeout = null; 33847 is_brush_or_erase_active = false; 33848 _bound_update_cursor = null; 33849 _bound_check_cursor_over_image = null; 33850 /** 33851 * Returns whether the cursor is currently over the image. 33852 */ 33853 is_over_image() { 33854 return this.is_cursor_over_image; 33855 } 33856 /** 33857 * Sets up cursor-related event listeners. 33858 */ 33859 setup_event_listeners() { 33860 this.cleanup_event_listeners(); 33861 this._bound_update_cursor = this.update_cursor_position.bind(this); 33862 this._bound_check_cursor_over_image = this.check_cursor_over_image.bind(this); 33863 const stage = this.image_editor_context.app.stage; 33864 stage.on("pointermove", this._bound_update_cursor); 33865 stage.on("pointermove", this._bound_check_cursor_over_image); 33866 this.image_editor_context.image_container.on( 33867 "pointerenter", 33868 this.on_image_container_pointer_enter.bind(this) 33869 ); 33870 this.image_editor_context.image_container.on( 33871 "pointerleave", 33872 this.on_image_container_pointer_leave.bind(this) 33873 ); 33874 } 33875 /** 33876 * Removes all cursor-related event listeners. 33877 */ 33878 cleanup_event_listeners() { 33879 const stage = this.image_editor_context.app.stage; 33880 if (this._bound_update_cursor) { 33881 stage.off("pointermove", this._bound_update_cursor); 33882 this._bound_update_cursor = null; 33883 } 33884 if (this._bound_check_cursor_over_image) { 33885 stage.off("pointermove", this._bound_check_cursor_over_image); 33886 this._bound_check_cursor_over_image = null; 33887 } 33888 this.image_editor_context.image_container.off("pointerenter"); 33889 this.image_editor_context.image_container.off("pointerleave"); 33890 this.cursor_position_check_timeout = clear_timeout( 33891 this.cursor_position_check_timeout 33892 ); 33893 } 33894 /** 33895 * initializes the cursor for the brush tool. 33896 */
33897 initialize_cursor() { 33898 if (this.cursor_container) { 33899 if (this.cursor_container.parent) { 33900 this.cursor_container.parent.removeChild(this.cursor_container); 33901 } 33902 this.cursor_container.destroy({ children: true }); 33903 this.cursor_container = null; 33904 } 33905 this.cursor_container = new Container(); 33906 this.image_editor_context.ui_container.addChild(this.cursor_container); 33907 this.cursor_graphics = new Graphics(); 33908 this.cursor_container.addChild(this.cursor_graphics); 33909 this.update_cursor_appearance(); 33910 } 33911 /** 33912 * updates the appearance of the cursor. 33913 */ 33914 update_cursor_appearance() { 33915 if (!this.cursor_graphics) return; 33916 this.cursor_graphics.clear(); 33917 const cursor_color = this.state.mode === "draw" ? tinycolor(this.state.color).toString() : 16777215; 33918 this.cursor_graphics.circle(0, 0, this.state.brush_size * this.scale).stroke({ 33919 width: 1.5, 33920 color: cursor_color, 33921 alpha: 0.8 33922 }); 33923 this.cursor_graphics.circle(0, 0, 1).fill({ 33924 color: cursor_color, 33925 alpha: 0.8 33926 }); 33927 } 33928 /** 33929 * updates the position of the cursor. 33930 * @param {FederatedPointerEvent} event - The pointer event. 33931 */ 33932 update_cursor_position(event) { 33933 if (!this.cursor_container) return; 33934 const local_pos = this.image_editor_context.image_container.toLocal( 33935 event.global 33936 ); 33937 const ui_pos = this.image_editor_context.ui_container.toLocal( 33938 this.image_editor_context.image_container.toGlobal(local_pos) 33939 ); 33940 this.cursor_container.position.set(ui_pos.x, ui_pos.y); 33941 } 33942 /** 33943 * checks if the cursor is over the image container. 33944 * @param {FederatedPointerEvent} event - The pointer event. 33945 */ 33946 check_cursor_over_image(event) { 33947 if (this.cursor_position_check_timeout !== null) { 33948 window.clearTimeout(this.cursor_position_check_timeout); 33949 this.cursor_position_check_timeout = null; 33950 } 33951 const image_container = this.image_editor_context.image_container; 33952 const bounds = image_container.getBounds(); 33953 const was_over_image = this.is_cursor_over_image; 33954 this.is_cursor_over_image = event.global.x >= bounds.x && event.global.x <= bounds.x + bounds.width && event.global.y >= bounds.y && event.global.y <= bounds.y + bounds.height; 33955 if (was_over_image !== this.is_cursor_over_image) { 33956 this.update_cursor_and_preview_visibility(); 33957 } 33958 } 33959 /** 33960 * updates the size of the cursor. 33961 */ 33962 update_cursor_size() { 33963 this.update_cursor_appearance(); 33964 } 33965 /** 33966 * Updates whether brush or erase tool is active 33967 */ 33968 set_active(is_active) { 33969 this.is_brush_or_erase_active = is_active; 33970 this.update_cursor_and_preview_visibility(); 33971 } 33972 /** 33973 * updates the visibility of the cursor. 33974 */ 33975 update_cursor_visibility() { 33976 if (this.cursor_container) { 33977 this.cursor_container.visible = this.is_cursor_over_image && this.is_brush_or_erase_active; 33978 } 33979 } 33980 /** 33981 * Shows or hides the brush preview. 33982 * @param {boolean} show - Whether to show the preview. 33983 */ 33984 preview_brush(show) { 33985 this.is_preview_visible = show; 33986 if (this.brush_preview_container) { 33987 this.brush_preview_container.visible = show && this.is_brush_or_erase_active; 33988 } 33989 if (show) { 33990 this.update_brush_preview(); 33991 this.update_brush_preview_position(); 33992 } 33993 } 33994 /** 33995 * initializes the brush preview. 33996 */ 33997 initialize_brush_preview() { 33998 if (this.brush_preview_container) { 33999 if (this.brush_preview_container.parent) { 34000 this.brush_preview_container.parent.removeChild( 34001 this.brush_preview_container 34002 ); 34003 } 34004 this.brush_preview_container.destroy({ children: true }); 34005 this.brush_preview_container = null; 34006 } 34007 this.brush_preview_container = new Container(); 34008 this.image_editor_context.ui_container.addChild( 34009 this.brush_preview_container 34010 ); 34011 this.brush_preview_graphics = new Graphics(); 34012 this.brush_preview_container.addChild(this.brush_preview_graphics); 34013 this.brush_preview_container.visible = false; 34014 } 34015 /** 34016 * updates the brush preview appearance. 34017 */ 34018 update_brush_preview() { 34019 if (!this.brush_preview_graphics) return; 34020 this.brush_preview_graphics.clear(); 34021 const preview_size = this.state.brush_size * this.scale; 34022 const preview_color = this.state.mode === "draw" ? tinycolor(this.state.color).setAlpha(this.state.opacity).toString() : 16777215; 34023 this.brush_preview_graphics.circle(0, 0, preview_size).fill({ 34024 color: preview_color, 34025 alpha: this.state.mode === "draw" ? this.state.opacity : 0.3 34026 }); 34027 this.brush_preview_graphics.circle(0, 0, preview_size + 1).stroke({ 34028 width: 1, 34029 color: 0, 34030 alpha: 0.5 34031 }); 34032 } 34033 /** 34034 * updates the position of the brush preview to center of canvas. 34035 */ 34036 update_brush_preview_position() { 34037 if (!this.brush_preview_container) return;
34038 const image_container = this.image_editor_context.image_container; 34039 const center_x = image_container.width / 2; 34040 const center_y = image_container.height / 2; 34041 const global_pos = image_container.toGlobal({ x: center_x, y: center_y }); 34042 const ui_pos = this.image_editor_context.ui_container.toLocal(global_pos); 34043 this.brush_preview_container.position.set(ui_pos.x, ui_pos.y); 34044 } 34045 /** 34046 * Called when the pointer enters the image container. 34047 */ 34048 on_image_container_pointer_enter() { 34049 this.is_cursor_over_image = true; 34050 this.update_cursor_and_preview_visibility(); 34051 } 34052 /** 34053 * Called when the pointer leaves the image container. 34054 */ 34055 on_image_container_pointer_leave() { 34056 this.is_cursor_over_image = false; 34057 this.update_cursor_and_preview_visibility(); 34058 } 34059 /** 34060 * Updates both cursor and preview visibility based on cursor position. 34061 */ 34062 update_cursor_and_preview_visibility() { 34063 this.update_cursor_visibility(); 34064 if (this.brush_preview_container) { 34065 this.brush_preview_container.visible = this.is_preview_visible && this.is_brush_or_erase_active; 34066 } 34067 } 34068 /** 34069 * Cleans up all cursor resources. 34070 */ 34071 cleanup() { 34072 this.is_brush_or_erase_active = false; 34073 if (this.cursor_container) { 34074 if (this.cursor_container.parent) { 34075 this.cursor_container.parent.removeChild(this.cursor_container); 34076 } 34077 this.cursor_container.destroy({ children: true }); 34078 this.cursor_container = null; 34079 } 34080 if (this.brush_preview_container) { 34081 if (this.brush_preview_container.parent) { 34082 this.brush_preview_container.parent.removeChild( 34083 this.brush_preview_container 34084 ); 34085 } 34086 this.brush_preview_container.destroy({ children: true }); 34087 this.brush_preview_container = null; 34088 } 34089 if (this.cursor_position_check_timeout !== null) { 34090 window.clearTimeout(this.cursor_position_check_timeout); 34091 this.cursor_position_check_timeout = null; 34092 } 34093 } 34094 /** 34095 * Updates the brush state. 34096 */ 34097 update_state(state, scale) { 34098 this.state = state; 34099 this.scale = scale; 34100 this.update_cursor_appearance(); 34101 if (this.is_preview_visible) { 34102 this.update_brush_preview(); 34103 } 34104 } 34105} 34106 34107class BrushCommand { 34108 stroke_data; 34109 context; 34110 original_texture = null; 34111 name; 34112 constructor(context, stroke_data, original_texture) { 34113 this.name = "Draw"; 34114 this.stroke_data = stroke_data; 34115 this.context = context; 34116 if (original_texture) { 34117 this.original_texture = this.create_texture_from(original_texture); 34118 } 34119 } 34120 /** 34121 * Creates a new texture with the same content as the source texture 34122 */ 34123 create_texture_from(source) { 34124 const texture = RenderTexture.create({ 34125 width: source.width, 34126 height: source.height, 34127 resolution: window.devicePixelRatio || 1 34128 }); 34129 const sprite = new Sprite(source); 34130 const container = new Container(); 34131 container.addChild(sprite); 34132 this.context.app.renderer.render(container, { 34133 renderTexture: texture 34134 }); 34135 container.destroy({ children: true }); 34136 return texture; 34137 } 34138 /** 34139 * Recreates the stroke by rendering all segments from the stored parameters 34140 * Preserves the exact order of draw/erase operations 34141 */ 34142 render_stroke_from_data(stroke_data, target_texture) { 34143 const draw_segments = stroke_data.segments.filter((s) => s.mode === "draw"); 34144 const erase_segments = stroke_data.segments.filter( 34145 (s) => s.mode === "erase" 34146 ); 34147 if (draw_segments.length > 0) { 34148 const graphics = new Graphics(); 34149 const container = new Container(); 34150 container.addChild(graphics); 34151 let alpha = 1; 34152 for (const segment of draw_segments) { 34153 if (segment.opacity < alpha) { 34154 alpha = segment.opacity; 34155 } 34156 let colorValue = 16777215; 34157 if (segment.color.startsWith("#")) { 34158 colorValue = parseInt(segment.color.replace("#", "0x"), 16); 34159 } 34160 graphics.setFillStyle({ 34161 color: colorValue, 34162 alpha: 1 34163 }); 34164 this.render_segment_to_graphics(graphics, segment); 34165 }
34166 const alpha_sprite_texture = RenderTexture.create({ 34167 width: target_texture.width, 34168 height: target_texture.height, 34169 resolution: window.devicePixelRatio || 1 34170 }); 34171 const alpha_sprite = new Sprite(alpha_sprite_texture); 34172 this.context.app.renderer.render({ 34173 container, 34174 target: alpha_sprite_texture 34175 }); 34176 alpha_sprite.alpha = alpha; 34177 this.context.app.renderer.render({ 34178 container: alpha_sprite, 34179 target: target_texture, 34180 clear: false 34181 }); 34182 container.destroy({ children: true }); 34183 alpha_sprite.destroy(); 34184 alpha_sprite_texture.destroy(); 34185 } 34186 if (erase_segments.length > 0) { 34187 const temp_content_texture = RenderTexture.create({ 34188 width: target_texture.width, 34189 height: target_texture.height, 34190 resolution: window.devicePixelRatio || 1 34191 }); 34192 const copy_sprite = new Sprite(target_texture); 34193 const copy_container = new Container(); 34194 copy_container.addChild(copy_sprite); 34195 this.context.app.renderer.render({ 34196 container: copy_container, 34197 target: temp_content_texture, 34198 clear: true 34199 }); 34200 const erase_graphics = new Graphics(); 34201 const erase_container = new Container(); 34202 erase_container.addChild(erase_graphics); 34203 erase_graphics.setFillStyle({ 34204 color: 16777215, 34205 alpha: 1 34206 }); 34207 for (const segment of erase_segments) { 34208 this.render_segment_to_graphics(erase_graphics, segment); 34209 } 34210 const mask_texture = RenderTexture.create({ 34211 width: target_texture.width, 34212 height: target_texture.height, 34213 resolution: window.devicePixelRatio || 1 34214 }); 34215 this.context.app.renderer.render({ 34216 container: erase_container, 34217 target: mask_texture, 34218 clear: true 34219 }); 34220 const content_sprite = new Sprite(temp_content_texture); 34221 const mask_sprite = new Sprite(mask_texture); 34222 const masked_container = new Container(); 34223 masked_container.addChild(content_sprite); 34224 masked_container.setMask({ mask: mask_sprite, inverse: true }); 34225 this.context.app.renderer.render({ 34226 container: masked_container, 34227 target: target_texture, 34228 clear: true 34229 }); 34230 copy_container.destroy({ children: true }); 34231 erase_container.destroy({ children: true }); 34232 masked_container.destroy({ children: true }); 34233 temp_content_texture.destroy(); 34234 mask_texture.destroy(); 34235 } 34236 } 34237 /** 34238 * Renders a segment to a graphics object (extracted from renderSegment) 34239 */ 34240 render_segment_to_graphics(graphics, segment) { 34241 const distance = Math.sqrt( 34242 Math.pow(segment.to_x - segment.from_x, 2) + Math.pow(segment.to_y - segment.from_y, 2) 34243 ); 34244 if (distance < 0.1) { 34245 graphics.circle(segment.from_x, segment.from_y, segment.size).fill(); 34246 } else { 34247 const spacing = Math.max(segment.size / 3, 2); 34248 const steps = Math.max(Math.ceil(distance / spacing), 2); 34249 for (let i = 0; i < steps; i++) { 34250 const t = i / (steps - 1); 34251 const x = segment.from_x + (segment.to_x - segment.from_x) * t; 34252 const y = segment.from_y + (segment.to_y - segment.from_y) * t; 34253 graphics.circle(x, y, segment.size).fill(); 34254 } 34255 } 34256 } 34257 async execute(context) { 34258 if (context) { 34259 this.context = context; 34260 } 34261 let layer_textures = this.context.layer_manager.get_layer_textures( 34262 this.stroke_data.layer_id 34263 ); 34264 if (!layer_textures) { 34265 const all_layers = this.context.layer_manager.get_layers(); 34266 const top_layer = all_layers[all_layers.length - 1]; 34267 layer_textures = this.context.layer_manager.get_layer_textures( 34268 top_layer.id 34269 ); 34270 } 34271 if (!layer_textures) return; 34272 const temp_texture = RenderTexture.create({ 34273 width: layer_textures.draw.width, 34274 height: layer_textures.draw.height, 34275 resolution: window.devicePixelRatio || 1 34276 }); 34277 const current_sprite = new Sprite(layer_textures.draw); 34278 const temp_container = new Container(); 34279 temp_container.addChild(current_sprite); 34280 this.context.app.renderer.render({ 34281 container: temp_container, 34282 target: temp_texture, 34283 clear: true 34284 }); 34285 temp_container.destroy({ children: true }); 34286 this.render_stroke_from_data(this.stroke_data, temp_texture); 34287 const final_sprite = new Sprite(temp_texture); 34288 const final_container = new Container(); 34289 final_container.addChild(final_sprite); 34290 this.context.app.renderer.render({ 34291 container: final_container, 34292 target: layer_textures.draw, 34293 clear: true 34294 }); 34295 final_container.destroy({ children: true }); 34296 temp_texture.destroy(); 34297 } 34298 async undo() { 34299 if (!this.original_texture) return; 34300 const layer_textures = this.context.layer_manager.get_layer_textures( 34301 this.stroke_data.layer_id 34302 ); 34303 if (!layer_textures) return; 34304 const temp_sprite = new Sprite(this.original_texture); 34305 const temp_container = new Container(); 34306 temp_container.addChild(temp_sprite); 34307 this.context.app.renderer.render({ 34308 container: temp_container, 34309 target: layer_textures.draw, 34310 clear: true 34311 }); 34312 temp_container.destroy({ children: true }); 34313 } 34314} 34315class BrushTextures { 34316 stroke_texture = null; 34317 erase_texture = null; 34318 display_container = null; 34319 stroke_container = null; 34320 stroke_graphics = null; 34321 preview_sprite = null; 34322 erase_graphics = null; 34323 dimensions; 34324 image_editor_context; 34325 app; 34326 is_new_stroke = true; 34327 current_opacity = 1; 34328 original_layer_texture = null; 34329 active_layer_id = null; 34330 current_stroke_segments = []; 34331 constructor(image_editor_context, app) { 34332 this.image_editor_context = image_editor_context; 34333 this.app = app; 34334 this.dimensions = {
34335 width: this.image_editor_context.image_container.width, 34336 height: this.image_editor_context.image_container.height 34337 }; 34338 } 34339 /** 34340 * Initializes textures needed for the brush tool. 34341 */ 34342 initialize_textures() { 34343 this.cleanup_textures(); 34344 const local_bounds = this.image_editor_context.image_container.getLocalBounds(); 34345 this.dimensions = { 34346 width: local_bounds.width, 34347 height: local_bounds.height 34348 }; 34349 this.stroke_texture = RenderTexture.create({ 34350 width: this.dimensions.width, 34351 height: this.dimensions.height, 34352 resolution: window.devicePixelRatio || 1 34353 }); 34354 this.erase_texture = RenderTexture.create({ 34355 width: this.dimensions.width, 34356 height: this.dimensions.height, 34357 resolution: window.devicePixelRatio || 1 34358 }); 34359 this.display_container = new Container(); 34360 this.image_editor_context.image_container.addChild(this.display_container); 34361 this.stroke_container = new Container(); 34362 this.stroke_graphics = new Graphics(); 34363 this.stroke_container.addChild(this.stroke_graphics); 34364 this.erase_graphics = new Graphics(); 34365 this.preview_sprite = new Sprite(this.stroke_texture); 34366 this.preview_sprite.alpha = 0; 34367 this.display_container.addChild(this.preview_sprite); 34368 const clear_container = new Container(); 34369 this.app.renderer.render(clear_container, { 34370 renderTexture: this.stroke_texture 34371 }); 34372 this.app.renderer.render(clear_container, { 34373 renderTexture: this.erase_texture 34374 }); 34375 clear_container.destroy(); 34376 this.is_new_stroke = true; 34377 this.current_opacity = 1; 34378 } 34379 /** 34380 * Reinitializes textures when needed (e.g., after resizing). 34381 */ 34382 reinitialize() { 34383 if (this.image_editor_context.image_container.width !== this.dimensions.width || this.image_editor_context.image_container.height !== this.dimensions.height) { 34384 this.initialize_textures(); 34385 } 34386 } 34387 /** 34388 * Cleans up texture resources. 34389 */ 34390 cleanup_textures() { 34391 if (this.stroke_texture) { 34392 this.stroke_texture.destroy(); 34393 this.stroke_texture = null; 34394 } 34395 if (this.erase_texture) { 34396 this.erase_texture.destroy(); 34397 this.erase_texture = null; 34398 } 34399 if (this.display_container) { 34400 if (this.display_container.parent) { 34401 this.display_container.parent.removeChild(this.display_container); 34402 } 34403 this.display_container.destroy({ children: true }); 34404 this.display_container = null; 34405 } 34406 if (this.original_layer_texture) { 34407 this.original_layer_texture.destroy(); 34408 this.original_layer_texture = null; 34409 } 34410 this.stroke_container = null; 34411 this.stroke_graphics = null; 34412 this.preview_sprite = null; 34413 this.erase_graphics = null; 34414 this.active_layer_id = null; 34415 } 34416 /** 34417 * Preserve the canvas state when starting a new stroke. 34418 */ 34419 preserve_canvas_state() { 34420 const active_layer = this.image_editor_context.layer_manager.get_active_layer(); 34421 if (!active_layer) return; 34422 const layers = this.image_editor_context.layer_manager.get_layers(); 34423 const layer = layers.find((l) => l.container === active_layer); 34424 if (!layer) return; 34425 this.active_layer_id = layer.id; 34426 const layer_textures = this.image_editor_context.layer_manager.get_layer_textures(layer.id); 34427 if (!layer_textures) return; 34428 if (this.original_layer_texture) { 34429 this.original_layer_texture.destroy(); 34430 } 34431 this.original_layer_texture = RenderTexture.create({ 34432 width: this.dimensions.width, 34433 height: this.dimensions.height, 34434 resolution: window.devicePixelRatio || 1 34435 }); 34436 const temp_sprite = new Sprite(layer_textures.draw); 34437 const temp_container = new Container(); 34438 temp_container.addChild(temp_sprite); 34439 this.app.renderer.render(temp_container, { 34440 renderTexture: this.original_layer_texture 34441 }); 34442 temp_container.destroy({ children: true }); 34443 this.is_new_stroke = true; 34444 } 34445 /** 34446 * Resets the eraser mask. 34447 */ 34448 reset_eraser_mask() { 34449 if (!this.erase_graphics || !this.erase_texture) return; 34450 this.erase_graphics.clear(); 34451 this.erase_graphics.setFillStyle({ color: 16777215, alpha: 1 }); 34452 this.erase_graphics.rect(0, 0, this.dimensions.width, this.dimensions.height).fill(); 34453 this.erase_graphics.endFill(); 34454 this.app.renderer.render(this.erase_graphics, { 34455 renderTexture: this.erase_texture 34456 }); 34457 } 34458 /** 34459 * Commits the current stroke to the active layer and returns a command for undo/redo. 34460 */ 34461 commit_stroke() { 34462 if (!this.stroke_texture || !this.preview_sprite || !this.stroke_graphics || !this.original_layer_texture || !this.active_layer_id) 34463 return; 34464 this.preview_sprite.visible = false;
34465 const active_layer = this.image_editor_context.layer_manager.get_active_layer(); 34466 if (!active_layer) return; 34467 const layers = this.image_editor_context.layer_manager.get_layers(); 34468 const layer = layers.find((l) => l.container === active_layer); 34469 if (!layer) return; 34470 if (layer.id !== this.active_layer_id) return; 34471 const layer_textures = this.image_editor_context.layer_manager.get_layer_textures(layer.id); 34472 if (!layer_textures) return; 34473 const stroke_data = { 34474 segments: [...this.current_stroke_segments], 34475 layer_id: this.active_layer_id 34476 }; 34477 const brush_command = new BrushCommand( 34478 this.image_editor_context, 34479 stroke_data, 34480 this.original_layer_texture 34481 ); 34482 if (this.stroke_graphics) { 34483 this.stroke_graphics.clear(); 34484 } 34485 const clear_container = new Container(); 34486 this.app.renderer.render(clear_container, { 34487 renderTexture: this.stroke_texture 34488 }); 34489 clear_container.destroy(); 34490 this.is_new_stroke = true; 34491 this.original_layer_texture = null; 34492 this.active_layer_id = null; 34493 this.current_stroke_segments = []; 34494 this.image_editor_context.command_manager.execute( 34495 brush_command, 34496 this.image_editor_context 34497 ); 34498 } 34499 /** 34500 * Calculates the distance between two points. 34501 */ 34502 calculate_distance(x1, y1, x2, y2) { 34503 return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 34504 } 34505 /**
34506 * Draws a segment for the current stroke using a simple "stamp" approach. 34507 */ 34508 draw_segment(from_x, from_y, to_x, to_y, size, color, opacity, mode) { 34509 if (!this.stroke_graphics || !this.stroke_texture || !this.stroke_container || !this.preview_sprite) 34510 return; 34511 if (this.is_new_stroke && !this.original_layer_texture) { 34512 this.preserve_canvas_state(); 34513 } 34514 this.current_opacity = mode === "draw" ? Math.min(Math.max(opacity, 0), 1) : 0.5; 34515 const scaled_size = size; 34516 if (this.is_new_stroke) { 34517 this.stroke_graphics.clear(); 34518 this.current_stroke_segments = []; 34519 const clear_container = new Container(); 34520 this.app.renderer.render(clear_container, { 34521 renderTexture: this.stroke_texture 34522 }); 34523 clear_container.destroy(); 34524 this.is_new_stroke = false; 34525 } 34526 this.current_stroke_segments.push({ 34527 from_x, 34528 from_y, 34529 to_x, 34530 to_y, 34531 size: scaled_size, 34532 color, 34533 opacity: this.current_opacity, 34534 mode 34535 }); 34536 if (mode === "draw") { 34537 let colorValue = 16777215; 34538 try { 34539 if (color.startsWith("#")) { 34540 colorValue = parseInt(color.replace("#", "0x"), 16); 34541 } 34542 } catch (e) { 34543 colorValue = 16777215; 34544 } 34545 this.stroke_graphics.setFillStyle({ 34546 color: colorValue, 34547 alpha: 1 34548 }); 34549 } else { 34550 this.stroke_graphics.setFillStyle({ 34551 color: 16777215, 34552 alpha: 1 34553 }); 34554 } 34555 const distance = this.calculate_distance(from_x, from_y, to_x, to_y); 34556 if (distance < 0.1) { 34557 this.stroke_graphics.circle(from_x, from_y, scaled_size).fill(); 34558 } else { 34559 const spacing = Math.max(scaled_size / 3, 2); 34560 const steps = Math.max(Math.ceil(distance / spacing), 2); 34561 for (let i = 0; i < steps; i++) { 34562 const t = i / (steps - 1); 34563 const x = from_x + (to_x - from_x) * t; 34564 const y = from_y + (to_y - from_y) * t; 34565 this.stroke_graphics.circle(x, y, scaled_size).fill(); 34566 } 34567 } 34568 this.stroke_graphics.endFill(); 34569 this.app.renderer.render(this.stroke_container, { 34570 renderTexture: this.stroke_texture 34571 }); 34572 if (mode === "draw") { 34573 this.preview_sprite.texture = this.stroke_texture; 34574 this.preview_sprite.alpha = this.current_opacity; 34575 this.preview_sprite.tint = 16777215; 34576 } else { 34577 const active_layer = this.image_editor_context.layer_manager.get_active_layer(); 34578 if (!active_layer) return; 34579 const layers = this.image_editor_context.layer_manager.get_layers(); 34580 const layer = layers.find((l) => l.container === active_layer); 34581 if (!layer) return; 34582 const layer_textures = this.image_editor_context.layer_manager.get_layer_textures(layer.id); 34583 if (!layer_textures) return; 34584 const preview_texture = RenderTexture.create({ 34585 width: this.dimensions.width, 34586 height: this.dimensions.height, 34587 resolution: window.devicePixelRatio || 1 34588 }); 34589 const base_container = new Container(); 34590 const content_sprite = new Sprite(layer_textures.draw); 34591 base_container.addChild(content_sprite); 34592 this.app.renderer.render(base_container, { 34593 renderTexture: preview_texture 34594 }); 34595 base_container.destroy({ children: true }); 34596 const preview_container = new Container(); 34597 const mask_sprite = new Sprite(this.stroke_texture); 34598 const overlay = new Graphics(); 34599 overlay.setFillStyle({ color: 16777215, alpha: 0.5 }); 34600 overlay.rect(0, 0, this.dimensions.width, this.dimensions.height).fill(); 34601 overlay.setMask({ mask: mask_sprite, inverse: false }); 34602 preview_container.addChild(overlay); 34603 this.app.renderer.render(preview_container, { 34604 renderTexture: preview_texture 34605 }); 34606 this.preview_sprite.texture = preview_texture; 34607 this.preview_sprite.alpha = 1; 34608 preview_container.destroy({ children: true }); 34609 preview_texture.destroy(); 34610 } 34611 this.preview_sprite.visible = true; 34612 } 34613 /** 34614 * Gets current dimensions of the texture. 34615 */ 34616 get_dimensions() { 34617 return this.dimensions; 34618 } 34619 /** 34620 * Checks if textures are properly initialized. 34621 */ 34622 get textures_initialized() { 34623 return !!(this.stroke_texture && this.display_container && this.preview_sprite); 34624 } 34625 /** 34626 * Cleanup all resources. 34627 */ 34628 cleanup() { 34629 this.cleanup_textures(); 34630 } 34631} 34632 34633class BrushTool { 34634 /** 34635 * The name of the tool. 34636 * @type {string} 34637 */ 34638 name = "brush"; 34639 /** 34640 * The context of the image editor. 34641 * @private 34642 */ 34643 image_editor_context; 34644 /** 34645 * The current tool selected in the toolbar. 34646 * @private 34647 */ 34648 current_tool; 34649 /** 34650 * The current subtool selected in the toolbar. 34651 * @private 34652 */ 34653 current_subtool; 34654 /** 34655 * The state of the brush tool. 34656 * @private 34657 */ 34658 state = { 34659 opacity: 1, 34660 brush_size: 10, 34661 color: "#000000", 34662 mode: "draw" 34663 }; 34664 /** 34665 * Size settings for brush and eraser 34666 * @private 34667 */ 34668 brush_size = 10; 34669 eraser_size = 20; 34670 /** 34671 * Drawing state 34672 * @private 34673 */ 34674 is_drawing = false;
34675 last_x = 0; 34676 last_y = 0; 34677 scale = 1; 34678 /** 34679 * Event listeners 34680 * @private 34681 */ 34682 _bound_pointer_down = null; 34683 _bound_pointer_move = null; 34684 _bound_pointer_up = null; 34685 event_callbacks = /* @__PURE__ */ new Map(); 34686 /** 34687 * Utility modules 34688 * @private 34689 */ 34690 brush_cursor = null; 34691 brush_textures = null; 34692 /** 34693 * Sets up the brush tool with the given context, tool, and subtool. 34694 * @param {ImageEditorContext} context - The image editor context. 34695 * @param {ToolbarTool} tool - The tool from the toolbar. 34696 * @param {Subtool} subtool - The subtool from the toolbar. 34697 * @returns {Promise<void>} 34698 */ 34699 async setup(context, tool, subtool) { 34700 this.image_editor_context = context; 34701 this.current_tool = tool; 34702 this.current_subtool = subtool; 34703 this.state.mode = tool === "erase" ? "erase" : "draw"; 34704 if (this.state.mode === "draw") { 34705 this.state.brush_size = this.brush_size; 34706 } else { 34707 this.state.brush_size = this.eraser_size; 34708 } 34709 context.scale.subscribe((scale) => { 34710 this.scale = scale; 34711 if (this.brush_cursor) { 34712 this.brush_cursor.update_state(this.state, this.scale); 34713 } 34714 }); 34715 this.brush_cursor = new BrushCursor( 34716 this.image_editor_context, 34717 this.state, 34718 this.scale 34719 ); 34720 this.brush_cursor.set_active(tool === "draw" || tool === "erase"); 34721 this.brush_textures = new BrushTextures( 34722 this.image_editor_context, 34723 context.app 34724 ); 34725 this.brush_textures.initialize_textures(); 34726 this.setup_event_listeners(); 34727 this.handle_cursors(tool); 34728 } 34729 /** 34730 * Handles cursor styles for the brush tool. 34731 * @param {ToolbarTool} tool - The current tool. 34732 * @private 34733 */ 34734 handle_cursors(tool) { 34735 recurse_set_cursor( 34736 this.image_editor_context.image_container.children, 34737 "none" 34738 ); 34739 } 34740 /** 34741 * Sets the current tool and subtool. 34742 * @param {ToolbarTool} tool - The current tool. 34743 * @param {Subtool} subtool - The current subtool. 34744 */ 34745 set_tool(tool, subtool) { 34746 this.current_tool = tool; 34747 this.current_subtool = subtool; 34748 if (this.current_tool !== "erase" && this.current_tool !== "draw") { 34749 this.commit_pending_changes(); 34750 } 34751 if (this.brush_cursor) { 34752 const should_be_active = tool === "draw" || tool === "erase"; 34753 this.brush_cursor.set_active(should_be_active); 34754 } 34755 const new_mode = tool === "erase" ? "erase" : "draw"; 34756 const mode_changed = this.state.mode !== new_mode; 34757 const needs_brush_tool = tool === "erase" || tool === "draw"; 34758 const textures_initialized = this.brush_textures?.textures_initialized ?? false; 34759 if (needs_brush_tool && (mode_changed || !textures_initialized)) { 34760 this.brush_textures?.initialize_textures(); 34761 } 34762 if (this.state.mode !== new_mode) { 34763 this.state.mode = new_mode; 34764 if (this.state.mode === "draw") { 34765 this.state.brush_size = this.brush_size; 34766 } else { 34767 this.state.brush_size = this.eraser_size; 34768 } 34769 if (this.brush_cursor) { 34770 this.brush_cursor.update_state(this.state, this.scale); 34771 } 34772 } 34773 } 34774 /** 34775 * Commits any pending changes to the canvas. 34776 * @private 34777 */ 34778 commit_pending_changes() { 34779 if (this.is_drawing) { 34780 this.on_pointer_up(); 34781 } 34782 } 34783 /** 34784 * Called when the user presses the mouse button. 34785 * @param {FederatedPointerEvent} event - The pointer event. 34786 * @private 34787 */ 34788 on_pointer_down(event) { 34789 const current_layer = this.image_editor_context.layer_manager.get_active_layer(); 34790 if (!current_layer?.visible || this.current_tool !== "erase" && this.current_tool !== "draw") { 34791 return; 34792 } 34793 if (this.brush_cursor && !this.brush_cursor.is_over_image()) { 34794 return; 34795 } 34796 if (this.brush_textures) { 34797 this.brush_textures.preserve_canvas_state(); 34798 } 34799 const local_pos = this.image_editor_context.image_container.toLocal( 34800 event.global 34801 ); 34802 this.is_drawing = true; 34803 this.last_x = local_pos.x; 34804 this.last_y = local_pos.y; 34805 if (this.brush_textures) { 34806 this.brush_textures.draw_segment( 34807 local_pos.x, 34808 local_pos.y, 34809 local_pos.x, 34810 local_pos.y, 34811 this.state.brush_size, 34812 this.state.color, 34813 this.state.opacity, 34814 this.state.mode 34815 ); 34816 } 34817 } 34818 /** 34819 * Called when the user moves the mouse while drawing. 34820 * @param {FederatedPointerEvent} event - The pointer event. 34821 * @private 34822 */ 34823 on_pointer_move(event) {
34824 if (this.brush_cursor) { 34825 this.brush_cursor.update_cursor_position(event); 34826 } 34827 if (!this.is_drawing) return; 34828 const local_pos = this.image_editor_context.image_container.toLocal( 34829 event.global 34830 ); 34831 if (this.brush_textures) { 34832 this.brush_textures.draw_segment( 34833 this.last_x, 34834 this.last_y, 34835 local_pos.x, 34836 local_pos.y, 34837 this.state.brush_size, 34838 this.state.color, 34839 this.state.opacity, 34840 this.state.mode 34841 ); 34842 } 34843 this.last_x = local_pos.x; 34844 this.last_y = local_pos.y; 34845 } 34846 /** 34847 * Called when the user releases the mouse button. 34848 * @private 34849 */ 34850 on_pointer_up() { 34851 if (!this.is_drawing) return; 34852 this.is_drawing = false; 34853 if (this.brush_textures) { 34854 this.brush_textures.commit_stroke(); 34855 } 34856 this.notify("change"); 34857 } 34858 /** 34859 * Sets up event listeners for drawing. 34860 * @private 34861 */ 34862 setup_event_listeners() { 34863 this.cleanup_event_listeners(); 34864 this._bound_pointer_down = this.on_pointer_down.bind(this); 34865 this._bound_pointer_move = this.on_pointer_move.bind(this); 34866 this._bound_pointer_up = this.on_pointer_up.bind(this); 34867 const image_container = this.image_editor_context.image_container; 34868 image_container.eventMode = "static"; 34869 image_container.interactiveChildren = true; 34870 const stage = this.image_editor_context.app.stage; 34871 stage.eventMode = "static"; 34872 stage.on("pointerdown", this._bound_pointer_down); 34873 stage.on("pointermove", this._bound_pointer_move); 34874 stage.on("pointerup", this._bound_pointer_up); 34875 stage.on("pointerupoutside", this._bound_pointer_up); 34876 if (this.brush_cursor) { 34877 this.brush_cursor.setup_event_listeners(); 34878 } 34879 } 34880 /** 34881 * Cleans up event listeners. 34882 * @private 34883 */ 34884 cleanup_event_listeners() { 34885 const stage = this.image_editor_context.app.stage; 34886 if (this._bound_pointer_down) { 34887 stage.off("pointerdown", this._bound_pointer_down); 34888 this._bound_pointer_down = null; 34889 } 34890 if (this._bound_pointer_move) { 34891 stage.off("pointermove", this._bound_pointer_move); 34892 this._bound_pointer_move = null; 34893 } 34894 if (this._bound_pointer_up) { 34895 stage.off("pointerup", this._bound_pointer_up); 34896 stage.off("pointerupoutside", this._bound_pointer_up); 34897 this._bound_pointer_up = null; 34898 } 34899 if (this.brush_cursor) { 34900 this.brush_cursor.cleanup_event_listeners(); 34901 } 34902 } 34903 /** 34904 * Sets the brush size. 34905 * @param {number} size - The new brush size. 34906 */ 34907 set_brush_size(size) { 34908 this.brush_size = size; 34909 if (this.state.mode === "draw") { 34910 this.state.brush_size = size; 34911 if (this.brush_cursor) { 34912 this.brush_cursor.update_state(this.state, this.scale); 34913 } 34914 } 34915 } 34916 /** 34917 * Sets the brush color. 34918 * @param {string|ColorInput} color - The new brush color. 34919 */ 34920 set_brush_color(color) { 34921 const color_string = tinycolor(color).toHexString(); 34922 this.state.color = color_string; 34923 if (this.brush_cursor) { 34924 this.brush_cursor.update_state(this.state, this.scale); 34925 } 34926 } 34927 /** 34928 * Sets the brush opacity. 34929 * @param {number} opacity - The new brush opacity. 34930 */ 34931 set_brush_opacity(opacity) { 34932 const clamped_opacity = Math.max(0, Math.min(1, opacity)); 34933 this.state.opacity = clamped_opacity; 34934 if (this.brush_cursor) { 34935 this.brush_cursor.update_state(this.state, this.scale); 34936 } 34937 } 34938 /** 34939 * Sets the eraser size. 34940 * @param {number} size - The new eraser size. 34941 */ 34942 set_eraser_size(size) { 34943 this.eraser_size = size; 34944 if (this.state.mode === "erase") { 34945 this.state.brush_size = size; 34946 if (this.brush_cursor) { 34947 this.brush_cursor.update_state(this.state, this.scale); 34948 } 34949 } 34950 } 34951 /** 34952 * Gets the current size based on mode (brush or eraser). 34953 * @returns {number} The current size. 34954 */ 34955 get_current_size() { 34956 return this.state.mode === "draw" ? this.brush_size : this.eraser_size; 34957 } 34958 /** 34959 * Shows or hides the brush preview. 34960 * @param {boolean} show - Whether to show the preview. 34961 */ 34962 preview_brush(show) {
34963 if (this.brush_cursor) { 34964 this.brush_cursor.preview_brush(show); 34965 } 34966 } 34967 /** 34968 * Cleans up resources used by the brush tool. 34969 */ 34970 cleanup() { 34971 this.commit_pending_changes(); 34972 this.cleanup_event_listeners(); 34973 if (this.brush_textures) { 34974 this.brush_textures.cleanup(); 34975 this.brush_textures = null; 34976 } 34977 if (this.brush_cursor) { 34978 this.brush_cursor.cleanup(); 34979 this.brush_cursor = null; 34980 } 34981 } 34982 on(event, callback) { 34983 this.event_callbacks.set(event, [ 34984 ...this.event_callbacks.get(event) || [], 34985 callback 34986 ]); 34987 } 34988 off(event, callback) { 34989 this.event_callbacks.set( 34990 event, 34991 this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || [] 34992 ); 34993 } 34994 notify(event) { 34995 for (const callback of this.event_callbacks.get(event) || []) { 34996 callback(); 34997 } 34998 } 34999} 35000 35001var root_6$1 = from_html(`<div class="svelte-189k4ah"><!> <!> <!></div>`); 35002var root_3$2 = from_html(`<li class="svelte-189k4ah"><!> <button> </button> <!></li>`); 35003var root_10$1 = from_html(`<li class="add-layer svelte-189k4ah"><!></li>`); 35004var root_2$2 = from_html(`<ul class="svelte-189k4ah"><!> <!></ul>`); 35005var root_1$3 = from_html(`<div><button class="layer-title-button svelte-189k4ah" aria-label="Show Layers"> <span class="icon svelte-189k4ah"><!></span></button> <!></div>`); 35006 35007function Layers_1($$anchor, $$props) { 35008 push($$props, false); 35009 35010 const $layers = () => store_get(layers(), '$layers', $$stores); 35011 const [$$stores, $$cleanup] = setup_stores(); 35012 const dispatch = createEventDispatcher(); 35013 let layers = prop($$props, 'layers', 12); 35014 let enable_additional_layers = prop($$props, 'enable_additional_layers', 12, true); 35015 let enable_layers = prop($$props, 'enable_layers', 12, true); 35016 let show_layers = prop($$props, 'show_layers', 12, false); 35017 35018 function new_layer() { 35019 dispatch("new_layer"); 35020 } 35021 35022 function change_layer(id) { 35023 dispatch("change_layer", id); 35024 show_layers(false); 35025 } 35026 35027 function move_layer(id, direction) { 35028 dispatch("move_layer", { id, direction }); 35029 } 35030 35031 function delete_layer(id) { 35032 dispatch("delete_layer", id); 35033 } 35034 35035 var $$exports = { 35036 get layers() { 35037 return layers(); 35038 }, 35039 35040 set layers($$value) { 35041 layers($$value); 35042 flushSync(); 35043 }, 35044 35045 get enable_additional_layers() { 35046 return enable_additional_layers(); 35047 }, 35048 35049 set enable_additional_layers($$value) { 35050 enable_additional_layers($$value); 35051 flushSync(); 35052 }, 35053 35054 get enable_layers() { 35055 return enable_layers(); 35056 }, 35057 35058 set enable_layers($$value) { 35059 enable_layers($$value); 35060 flushSync(); 35061 }, 35062 35063 get show_layers() { 35064 return show_layers(); 35065 }, 35066 35067 set show_layers($$value) { 35068 show_layers($$value); 35069 flushSync(); 35070 } 35071 }; 35072 35073 init$1(); 35074 35075 var fragment = comment(); 35076 var node = first_child(fragment); 35077 35078 { 35079 var consequent_7 = ($$anchor) => { 35080 var div = root_1$3(); 35081 let classes; 35082 var button = child(div); 35083 var text = child(button); 35084 var span = sibling(text); 35085 var node_1 = child(span); 35086 35087 Layers(node_1); 35088 reset(span); 35089 reset(button); 35090 35091 var node_2 = sibling(button, 2); 35092 35093 { 35094 var consequent_6 = ($$anchor) => { 35095 var ul = root_2$2(); 35096 var node_3 = child(ul); 35097 35098 each(node_3, 1, () => ($layers(), untrack(() => $layers().layers)), index, ($$anchor, $$item, i) => { 35099 let id = () => get($$item).id; 35100 let name = () => get($$item).name; 35101 let user_created = () => get($$item).user_created; 35102 let visible = () => get($$item).visible; 35103 var li = root_3$2(); 35104 var node_4 = child(li); 35105 35106 { 35107 var consequent = ($$anchor) => { 35108 IconButton$1($$anchor, { 35109 get Icon() { 35110 return VisibilityOff; 35111 }, 35112 size: 'small', 35113 onclick: (e) => { 35114 e.stopPropagation(); 35115 dispatch("toggle_layer_visibility", id()); 35116 } 35117 }); 35118 }; 35119 35120 var alternate = ($$anchor) => { 35121 IconButton$1($$anchor, { 35122 get Icon() { 35123 return Visibility; 35124 }, 35125 size: 'small', 35126 onclick: (e) => { 35127 e.stopPropagation(); 35128 dispatch("toggle_layer_visibility", id()); 35129 } 35130 }); 35131 }; 35132 35133 if_block(node_4, ($$render) => { 35134 if (!visible()) $$render(consequent); else $$render(alternate, false); 35135 }); 35136 } 35137 35138 var button_1 = sibling(node_4, 2); 35139 35140 set_attribute(button_1, 'aria-label', `layer-${i + 1}`); 35141
35142 let classes_1; 35143 var text_1 = child(button_1, true); 35144 35145 reset(button_1); 35146 35147 var node_5 = sibling(button_1, 2); 35148 35149 { 35150 var consequent_4 = ($$anchor) => { 35151 var div_1 = root_6$1(); 35152 var node_6 = child(div_1); 35153 35154 { 35155 var consequent_1 = ($$anchor) => { 35156 IconButton$1($$anchor, { 35157 onclick: (e) => { 35158 e.stopPropagation(); 35159 move_layer(id(), "up"); 35160 }, 35161 35162 get Icon() { 35163 return ArrowUp; 35164 }, 35165 size: 'x-small' 35166 }); 35167 }; 35168 35169 if_block(node_6, ($$render) => { 35170 if (i > 0) $$render(consequent_1); 35171 }); 35172 } 35173 35174 var node_7 = sibling(node_6, 2); 35175 35176 { 35177 var consequent_2 = ($$anchor) => { 35178 IconButton$1($$anchor, { 35179 onclick: (e) => { 35180 e.stopPropagation(); 35181 move_layer(id(), "down"); 35182 }, 35183 35184 get Icon() { 35185 return ArrowDown; 35186 }, 35187 size: 'x-small' 35188 }); 35189 }; 35190 35191 if_block(node_7, ($$render) => { 35192 if (( 35193 $layers(), 35194 untrack(() => i < $layers().layers.length - 1) 35195 )) $$render(consequent_2); 35196 }); 35197 } 35198 35199 var node_8 = sibling(node_7, 2); 35200 35201 { 35202 var consequent_3 = ($$anchor) => { 35203 IconButton$1($$anchor, { 35204 onclick: (e) => { 35205 e.stopPropagation(); 35206 delete_layer(id()); 35207 }, 35208 35209 get Icon() { 35210 return Clear; 35211 }, 35212 size: 'x-small' 35213 }); 35214 }; 35215 35216 if_block(node_8, ($$render) => { 35217 if (( 35218 $layers(), 35219 user_created(), 35220 untrack(() => $layers().layers.length > 1 && user_created()) 35221 )) $$render(consequent_3); 35222 }); 35223 } 35224 35225 reset(div_1); 35226 append($$anchor, div_1); 35227 }; 35228 35229 if_block(node_5, ($$render) => { 35230 if (($layers(), untrack(() => $layers().layers.length > 1))) $$render(consequent_4); 35231 }); 35232 } 35233 35234 reset(li); 35235 35236 template_effect(() => { 35237 classes_1 = set_class(button_1, 1, 'svelte-189k4ah', null, classes_1, { selected_layer: $layers().active_layer === id() }); 35238 set_text(text_1, name()); 35239 }); 35240 35241 event('click', button_1, stopPropagation(() => change_layer(id()))); 35242 append($$anchor, li); 35243 }); 35244 35245 var node_9 = sibling(node_3, 2); 35246 35247 { 35248 var consequent_5 = ($$anchor) => { 35249 var li_1 = root_10$1();
35250 var node_10 = child(li_1); 35251 35252 IconButton$1(node_10, { 35253 get Icon() { 35254 return Plus; 35255 }, 35256 label: 'Add Layer', 35257 onclick: (e) => { 35258 e.stopPropagation(); 35259 new_layer(); 35260 }, 35261 size: 'x-small' 35262 }); 35263 35264 reset(li_1); 35265 append($$anchor, li_1); 35266 }; 35267 35268 if_block(node_9, ($$render) => { 35269 if (enable_additional_layers()) $$render(consequent_5); 35270 }); 35271 } 35272 35273 reset(ul); 35274 append($$anchor, ul); 35275 }; 35276 35277 if_block(node_2, ($$render) => { 35278 if (show_layers()) $$render(consequent_6); 35279 }); 35280 } 35281 35282 reset(div); 35283 action(div, ($$node, $$action_arg) => click_outside?.($$node, $$action_arg), () => () => show_layers(false)); 35284 35285 template_effect( 35286 ($0) => { 35287 classes = set_class(div, 1, 'layer-wrap svelte-189k4ah', null, classes, { closed: !show_layers() }); 35288 set_text(text, `${$0 ?? ''} `); 35289 }, 35290 [ 35291 () => ( 35292 deep_read_state(show_layers()), 35293 $layers(), 35294 untrack(() => show_layers() 35295 ? "Layers" 35296 : $layers().layers.find((l) => l.id === $layers().active_layer)?.name) 35297 ) 35298 ] 35299 ); 35300 35301 event('click', button, stopPropagation(() => show_layers(!show_layers()))); 35302 append($$anchor, div); 35303 }; 35304 35305 if_block(node, ($$render) => { 35306 if (enable_layers()) $$render(consequent_7); 35307 }); 35308 } 35309 35310 append($$anchor, fragment); 35311 35312 var $$pop = pop($$exports); 35313 35314 $$cleanup(); 35315 35316 return $$pop; 35317} 35318 35319var root = from_html(`<div class="toolbar-wrap-wrap svelte-1357rcu"><div class="toolbar-wrap svelte-1357rcu"><!></div></div>`); 35320 35321function SecondaryToolbar($$anchor, $$props) { 35322 push($$props, false); 35323 35324 let layers = prop($$props, 'layers', 12); 35325 let enable_additional_layers = prop($$props, 'enable_additional_layers', 12, true); 35326 let enable_layers = prop($$props, 'enable_layers', 12, true); 35327 let show_layers = prop($$props, 'show_layers', 12, false); 35328 35329 var $$exports = { 35330 get layers() { 35331 return layers(); 35332 }, 35333 35334 set layers($$value) { 35335 layers($$value); 35336 flushSync(); 35337 }, 35338 35339 get enable_additional_layers() { 35340 return enable_additional_layers(); 35341 }, 35342 35343 set enable_additional_layers($$value) { 35344 enable_additional_layers($$value); 35345 flushSync(); 35346 }, 35347 35348 get enable_layers() { 35349 return enable_layers(); 35350 }, 35351 35352 set enable_layers($$value) { 35353 enable_layers($$value); 35354 flushSync(); 35355 }, 35356 35357 get show_layers() { 35358 return show_layers(); 35359 }, 35360 35361 set show_layers($$value) { 35362 show_layers($$value); 35363 flushSync(); 35364 } 35365 }; 35366 35367 var div = root(); 35368 var div_1 = child(div); 35369 var node = child(div_1); 35370 35371 { 35372 var consequent = ($$anchor) => { 35373 Layers_1($$anchor, { 35374 get layers() { 35375 return layers(); 35376 }, 35377 35378 get enable_additional_layers() { 35379 return enable_additional_layers(); 35380 }, 35381 35382 get enable_layers() { 35383 return enable_layers(); 35384 }, 35385 35386 get show_layers() { 35387 return show_layers(); 35388 }, 35389 35390 $$events: { 35391 new_layer($$arg) { 35392 bubble_event.call(this, $$props, $$arg); 35393 }, 35394 35395 change_layer($$arg) { 35396 bubble_event.call(this, $$props, $$arg); 35397 }, 35398 35399 move_layer($$arg) { 35400 bubble_event.call(this, $$props, $$arg); 35401 }, 35402 35403 delete_layer($$arg) { 35404 bubble_event.call(this, $$props, $$arg); 35405 }, 35406 35407 toggle_layer_visibility($$arg) { 35408 bubble_event.call(this, $$props, $$arg); 35409 } 35410 } 35411 }); 35412 }; 35413 35414 if_block(node, ($$render) => { 35415 if (enable_layers()) $$render(consequent); 35416 }); 35417 } 35418 35419 reset(div_1); 35420 reset(div); 35421 append($$anchor, div); 35422 35423 return pop($$exports); 35424} 35425 35426var root_4$1 = from_html(`<li class="svelte-o0h5ih"><button class="svelte-o0h5ih"> </button></li>`); 35427var root_3$1 = from_html(`<div class="zoom-controls svelte-o0h5ih"><ul class="svelte-o0h5ih"><li class="svelte-o0h5ih"><button class="svelte-o0h5ih">Fit to screen</button></li> <!></ul></div>`); 35428var root_1$2 = from_html(`<!> <!> <!> <!> <div class="zoom-number svelte-o0h5ih"><span role="button" tabindex="0" class="svelte-o0h5ih"> </span> <!></div> <div class="separator svelte-o0h5ih"></div> <!> <!> <!> <!>`, 1); 35429 35430function Controls($$anchor, $$props) { 35431 push($$props, false); 35432 35433 const formatted_zoom = mutable_source(); 35434 let can_save = prop($$props, 'can_save', 12, false); 35435 let changeable = prop($$props, 'changeable', 12, false);
35436 let current_zoom = prop($$props, 'current_zoom', 12, 1); 35437 let tool = prop($$props, 'tool', 12); 35438 let min_zoom = prop($$props, 'min_zoom', 12, true); 35439 let enable_download = prop($$props, 'enable_download', 12, false); 35440 let can_undo = prop($$props, 'can_undo', 12); 35441 let can_redo = prop($$props, 'can_redo', 12); 35442 const dispatch = createEventDispatcher(); 35443 let show_zoom_popup = mutable_source(false); 35444 35445 function handle_zoom_click(e) { 35446 e.stopPropagation(); 35447 set(show_zoom_popup, !get(show_zoom_popup)); 35448 } 35449 35450 function handle_zoom_change(zoom) { 35451 dispatch("set_zoom", zoom); 35452 set(show_zoom_popup, false); 35453 } 35454 35455 function handle_zoom_keydown(e) { 35456 if (e.key === "Enter") { 35457 handle_zoom_change(current_zoom()); 35458 } 35459 } 35460 35461 legacy_pre_effect(() => (deep_read_state(current_zoom())), () => { 35462 set(formatted_zoom, Math.round(current_zoom() * 100)); 35463 }); 35464 35465 legacy_pre_effect_reset(); 35466 35467 var $$exports = { 35468 get can_save() { 35469 return can_save(); 35470 }, 35471 35472 set can_save($$value) { 35473 can_save($$value); 35474 flushSync(); 35475 }, 35476 35477 get changeable() { 35478 return changeable(); 35479 }, 35480 35481 set changeable($$value) { 35482 changeable($$value); 35483 flushSync(); 35484 }, 35485 35486 get current_zoom() { 35487 return current_zoom(); 35488 }, 35489 35490 set current_zoom($$value) { 35491 current_zoom($$value); 35492 flushSync(); 35493 }, 35494 35495 get tool() { 35496 return tool(); 35497 }, 35498 35499 set tool($$value) { 35500 tool($$value); 35501 flushSync(); 35502 }, 35503 35504 get min_zoom() { 35505 return min_zoom(); 35506 }, 35507 35508 set min_zoom($$value) { 35509 min_zoom($$value); 35510 flushSync(); 35511 }, 35512 35513 get enable_download() { 35514 return enable_download(); 35515 }, 35516 35517 set enable_download($$value) { 35518 enable_download($$value); 35519 flushSync(); 35520 }, 35521 35522 get can_undo() { 35523 return can_undo(); 35524 }, 35525 35526 set can_undo($$value) { 35527 can_undo($$value); 35528 flushSync(); 35529 }, 35530 35531 get can_redo() { 35532 return can_redo(); 35533 }, 35534 35535 set can_redo($$value) { 35536 can_redo($$value); 35537 flushSync(); 35538 } 35539 }; 35540 35541 init$1(); 35542 35543 IconButtonWrapper($$anchor, { 35544 children: ($$anchor, $$slotProps) => { 35545 var fragment_1 = root_1$2(); 35546 var node = first_child(fragment_1); 35547 35548 { 35549 var consequent = ($$anchor) => { 35550 IconButton$1($$anchor, { 35551 get Icon() { 35552 return Download; 35553 }, 35554 label: 'Download', 35555 onclick: (event) => { 35556 dispatch("download"); 35557 event.stopPropagation(); 35558 } 35559 }); 35560 }; 35561 35562 if_block(node, ($$render) => { 35563 if (enable_download()) $$render(consequent); 35564 }); 35565 } 35566 35567 var node_1 = sibling(node, 2); 35568 35569 { 35570 let $0 = derived_safe_equal(() => tool() === "pan"); 35571 35572 IconButton$1(node_1, { 35573 get Icon() { 35574 return Pan; 35575 }, 35576 label: 'Pan', 35577 onclick: (e) => { 35578 e.stopPropagation(); 35579 dispatch("pan"); 35580 }, 35581 35582 get highlight() { 35583 return get($0); 35584 }, 35585 size: 'small', 35586 padded: false, 35587 transparent: true, 35588 get disabled() { 35589 return min_zoom(); 35590 } 35591 }); 35592 } 35593 35594 var node_2 = sibling(node_1, 2); 35595 35596 IconButton$1(node_2, { 35597 get Icon() { 35598 return ZoomOut; 35599 }, 35600 label: 'Zoom out', 35601 onclick: (event) => { 35602 dispatch("zoom_out"); 35603 event.stopPropagation(); 35604 } 35605 }); 35606 35607 var node_3 = sibling(node_2, 2); 35608 35609 IconButton$1(node_3, { 35610 get Icon() { 35611 return ZoomIn; 35612 }, 35613 label: 'Zoom in', 35614 onclick: (event) => { 35615 dispatch("zoom_in"); 35616 event.stopPropagation(); 35617 } 35618 }); 35619 35620 var div = sibling(node_3, 2); 35621 var span = child(div); 35622 var text = child(span); 35623 35624 reset(span); 35625 35626 var node_4 = sibling(span, 2); 35627 35628 { 35629 var consequent_1 = ($$anchor) => { 35630 var div_1 = root_3$1(); 35631 var ul = child(div_1); 35632 var li = child(ul); 35633 var button = child(li); 35634 35635 reset(li); 35636 35637 var node_5 = sibling(li, 2); 35638 35639 each(node_5, 0, () => [0.25, 0.5, 1, 2, 4], index, ($$anchor, zoom) => { 35640 var li_1 = root_4$1(); 35641 var button_1 = child(li_1); 35642 var text_1 = child(button_1); 35643 35644 reset(button_1); 35645 reset(li_1); 35646 template_effect(() => set_text(text_1, `${zoom * 100}%`)); 35647 event('click', button_1, stopPropagation(() => handle_zoom_change(zoom))); 35648 append($$anchor, li_1); 35649 }); 35650 35651 reset(ul); 35652 reset(div_1); 35653 event('click', button, stopPropagation(() => handle_zoom_change("fit"))); 35654 append($$anchor, div_1); 35655 }; 35656 35657 if_block(node_4, ($$render) => { 35658 if (get(show_zoom_popup)) $$render(consequent_1); 35659 }); 35660 } 35661 35662 reset(div); 35663 35664 var node_6 = sibling(div, 4); 35665 35666 { 35667 let $0 = derived_safe_equal(() => !can_undo()); 35668 35669 IconButton$1(node_6, { 35670 get Icon() { 35671 return Undo; 35672 }, 35673 label: 'Undo', 35674 onclick: (event) => { 35675 dispatch("undo"); 35676 event.stopPropagation(); 35677 }, 35678 35679 get disabled() { 35680 return get($0); 35681 } 35682 }); 35683 } 35684 35685 var node_7 = sibling(node_6, 2); 35686 35687 { 35688 let $0 = derived_safe_equal(() => !can_redo()); 35689 35690 IconButton$1(node_7, {
35691 get Icon() { 35692 return Redo; 35693 }, 35694 label: 'Redo', 35695 onclick: (event) => { 35696 dispatch("redo"); 35697 event.stopPropagation(); 35698 }, 35699 35700 get disabled() { 35701 return get($0); 35702 } 35703 }); 35704 } 35705 35706 var node_8 = sibling(node_7, 2); 35707 35708 { 35709 var consequent_2 = ($$anchor) => { 35710 { 35711 let $0 = derived_safe_equal(() => !can_save()); 35712 35713 IconButton$1($$anchor, { 35714 get disabled() { 35715 return get($0); 35716 }, 35717 35718 get Icon() { 35719 return Check; 35720 }, 35721 label: 'Save changes', 35722 onclick: (event) => { 35723 dispatch("save"); 35724 event.stopPropagation(); 35725 }, 35726 color: 'var(--color-accent)' 35727 }); 35728 } 35729 }; 35730 35731 if_block(node_8, ($$render) => { 35732 if (changeable()) $$render(consequent_2); 35733 }); 35734 } 35735 35736 var node_9 = sibling(node_8, 2); 35737 35738 IconButton$1(node_9, { 35739 get Icon() { 35740 return Trash; 35741 }, 35742 label: 'Clear canvas', 35743 onclick: (event) => { 35744 dispatch("remove_image"); 35745 event.stopPropagation(); 35746 } 35747 }); 35748 35749 template_effect(() => set_text(text, `${get(formatted_zoom) ?? ''}%`)); 35750 event('click', span, handle_zoom_click); 35751 event('keydown', span, handle_zoom_keydown); 35752 append($$anchor, fragment_1); 35753 }, 35754 $$slots: { default: true } 35755 }); 35756 35757 return pop($$exports); 35758} 35759 35760var root_5$1 = from_html(`<div class="modal svelte-6fnl1z"><div class="modal-inner svelte-6fnl1z"><!></div></div>`); 35761var root_2$1 = from_html(`<!> <!> <!> <!>`, 1); 35762var root_7$1 = from_html(`<div class="crop-confirm-button svelte-6fnl1z"><!></div>`); 35763var root_1$1 = from_html(`<div data-testid="image" aria-dropeffect="copy"><!> <div></div> <div></div> <!> <!></div>`); 35764 35765function ImageEditor_1($$anchor, $$props) { 35766 push($$props, false); 35767 35768 const disable_click = mutable_source(); 35769 const { drag, open_file_upload } = create_drag(); 35770 const dispatch = createEventDispatcher(); 35771 const antialias = true; 35772 let changeable = prop($$props, 'changeable', 12, false); 35773 let sources = prop($$props, 'sources', 28, () => ["upload", "webcam", "clipboard"]); 35774 let transforms = prop($$props, 'transforms', 28, () => ["crop", "resize"]); 35775 let canvas_size = prop($$props, 'canvas_size', 12); 35776 let is_dragging = prop($$props, 'is_dragging', 12, false); 35777 let background_image = prop($$props, 'background_image', 12, false); 35778 let brush_options = prop($$props, 'brush_options', 12); 35779 let eraser_options = prop($$props, 'eraser_options', 12); 35780 let fixed_canvas = prop($$props, 'fixed_canvas', 12, false); 35781 let root = prop($$props, 'root', 12); 35782 let i18n = prop($$props, 'i18n', 12); 35783 let upload = prop($$props, 'upload', 12); 35784 let composite = prop($$props, 'composite', 12); 35785 let layers = prop($$props, 'layers', 12); 35786 let background = prop($$props, 'background', 12); 35787 let border_region = prop($$props, 'border_region', 12, 0); 35788 let layer_options = prop($$props, 'layer_options', 12); 35789 let current_tool = prop($$props, 'current_tool', 12); 35790 let webcam_options = prop($$props, 'webcam_options', 12); 35791 let show_download_button = prop($$props, 'show_download_button', 12, false); 35792 let theme_mode = prop($$props, 'theme_mode', 12); 35793 let full_history = prop($$props, 'full_history', 12, null); 35794 let pixi_target = mutable_source(); 35795 let pixi_target_crop = mutable_source(); 35796 35797 function refresh_tools() { 35798 if (!get(editor) || !get(ready)) return; 35799 35800 get(editor).set_tool(current_tool()); 35801 get(editor).set_subtool(get(current_subtool)); 35802 } 35803 35804 /** 35805 * Refreshes tools when layer state changes 35806 */ 35807 function refresh_tools_for_layer_changes(current_layers) { 35808 if (!get(editor) || !get(ready)) return; 35809 35810 if (current_layers.layers.length > 0 && !current_layers.active_layer) { 35811 get(editor).set_layer(current_layers.layers[0].id); 35812 } 35813 35814 // reapply current tool to ensure it targets the correct layer 35815 if (current_tool()) { 35816 get(editor).set_tool(current_tool()); 35817 35818 if (get(current_subtool)) { 35819 get(editor).set_subtool(get(current_subtool)); 35820 } 35821 } 35822 35823 if (get(brush) && (current_tool() === "draw" || current_tool() === "erase")) { 35824 get(brush).set_tool(current_tool(), get(current_subtool)); 35825 } 35826 } 35827
35828 function check_if_should_init() { 35829 return layer_options() && get(editor) && get(ready); 35830 } 35831 35832 let has_drawn = prop($$props, 'has_drawn', 12, false); 35833 35834 /** 35835 * Gets the image blobs from the editor 35836 * @returns {Promise<ImageBlobs>} Object containing background, layers, and composite image blobs 35837 */ 35838 async function get_blobs() { 35839 if (!get(editor)) return { background: null, layers: [], composite: null }; 35840 if (!background_image() && !has_drawn() && !layers().length) return { background: null, layers: [], composite: null }; 35841 35842 const blobs = await get(editor).get_blobs(); 35843 35844 return blobs; 35845 } 35846 35847 let editor = mutable_source(); 35848 35849 /** 35850 * Adds an image to the editor 35851 * @param {Blob | File} image - The image to add 35852 */ 35853 function add_image(image) { 35854 get(editor).add_image({ image }); 35855 } 35856 35857 let pending_bg; 35858 35859 /** 35860 * Adds an image to the editor from a URL 35861 * @param {string | FileData} source - The URL of the image or a FileData object 35862 * @returns {Promise<void>} 35863 */ 35864 async function add_image_from_url(source) { 35865 if (!get(editor) || !source || !check_if_should_init()) return; 35866 35867 let url; 35868 35869 if (typeof source === "string") { 35870 url = source; 35871 } else if (source?.meta?._type === "gradio.FileData" && source?.url) { 35872 url = source.url; 35873 } else { 35874 console.warn("Invalid source provided to add_image_from_url:", source); 35875 35876 return; 35877 } 35878 35879 try { 35880 pending_bg = get(editor).add_image_from_url(url); 35881 35882 let pending_crop = crop.add_image_from_url(url); 35883 35884 await Promise.all([pending_bg, pending_crop]); 35885 crop.set_tool("image"); 35886 crop.set_subtool("crop"); 35887 background_image(true); 35888 get(zoom).set_zoom("fit"); 35889 dispatch("upload"); 35890 dispatch("input"); 35891 } catch(error) { 35892 console.error("Error adding image from URL:", error); 35893 } 35894 } 35895 35896 /**
35897 * Adds a new layer with an image loaded from a URL 35898 * @param {string | FileData} source - The URL of the image or a FileData object 35899 * @returns {Promise<string | null>} - The ID of the created layer, or null if failed 35900 */ 35901 async function add_layers_from_url(source) { 35902 if (!get(editor) || !source.length || !check_if_should_init()) return; 35903 35904 if (Array.isArray(source) && source.every((item) => item?.meta?._type === "gradio.FileData")) { 35905 try { 35906 await pending_bg; 35907 await get(editor).add_layers_from_url(source.map((item) => item.url)); 35908 dispatch("change"); 35909 dispatch("input"); 35910 } catch(error) { 35911 console.error("Error adding layer from URL:", error); 35912 } 35913 } 35914 } 35915 35916 let brush = mutable_source(); 35917 let zoom = mutable_source(); 35918 let zoom_level = mutable_source(1); 35919 let ready = mutable_source(false); 35920 let min_zoom = mutable_source(true); 35921 let last_dimensions = { width: 0, height: 0 }; 35922 35923 /** 35924 * Handles visibility changes and resets zoom if dimensions have changed 35925 */ 35926 async function handle_visibility_change() { 35927 if (!get(editor) || !get(ready) || !get(zoom)) return; 35928 35929 await tick(); 35930 35931 const is_visible = get(pixi_target).offsetParent !== null; 35932 35933 if (is_visible) { 35934 const current_dimensions = get(pixi_target).getBoundingClientRect(); 35935 35936 if (current_dimensions.width !== last_dimensions.width || current_dimensions.height !== last_dimensions.height) { 35937 get(zoom).set_zoom("fit"); 35938 35939 last_dimensions = { 35940 width: current_dimensions.width, 35941 height: current_dimensions.height 35942 }; 35943 } 35944 } 35945 } 35946 35947 onMount(() => { 35948 let intersection_observer; 35949 let resize_observer; 35950 35951 init_image_editor().then(() => { 35952 35953 intersection_observer = new IntersectionObserver(() => { 35954 handle_visibility_change(); 35955 }); 35956 35957 resize_observer = new ResizeObserver(() => { 35958 handle_visibility_change(); 35959 }); 35960 35961 intersection_observer.observe(get(pixi_target)); 35962 resize_observer.observe(get(pixi_target)); 35963 35964 setTimeout( 35965 () => { 35966 if (full_history() && get(editor)) { 35967 get(editor).command_manager.replay(full_history(), get(editor).context).then(() => { 35968 refresh_tools_after_history(); 35969 }); 35970 } 35971 }, 35972 0 35973 ); 35974 }); 35975 35976 if (typeof window !== "undefined") { 35977 window.editor = get(editor); 35978 } 35979 35980 return () => { 35981 if (intersection_observer) { 35982 intersection_observer.disconnect(); 35983 } 35984 35985 if (resize_observer) { 35986 resize_observer.disconnect(); 35987 } 35988 35989 if (get(editor)) { 35990 get(editor).destroy(); 35991 } 35992 }; 35993 }); 35994 35995 /** 35996 * Refreshes tool state after history replay to ensure proper layer targeting 35997 */ 35998 function refresh_tools_after_history() { 35999 if (!get(editor) || !get(ready)) return; 36000 36001 const current_layers = get$1(get(editor).layers); 36002 36003 if (current_layers.layers.length > 0 && !current_layers.active_layer) { 36004 get(editor).set_layer(current_layers.layers[0].id); 36005 } 36006 36007 if (current_tool()) { 36008 get(editor).set_tool(current_tool()); 36009 36010 if (get(current_subtool)) { 36011 get(editor).set_subtool(get(current_subtool)); 36012 } 36013 } 36014 36015 if (get(brush) && (current_tool() === "draw" || current_tool() === "erase")) { 36016 get(brush).set_tool(current_tool(), get(current_subtool)); 36017 } 36018 36019 full_history(get(editor).command_manager.history); 36020 } 36021 36022 let crop; 36023 let crop_zoom = mutable_source(); 36024 let can_undo = prop($$props, 'can_undo', 12, false); 36025 let can_redo = mutable_source(false); 36026 36027 async function init_image_editor() { 36028 set(brush, new BrushTool()); 36029 set(zoom, new ZoomTool()); 36030 36031 set(editor, new ImageEditor({ 36032 target_element: get(pixi_target), 36033 width: canvas_size()[0], 36034 height: canvas_size()[1], 36035 tools: ["image", get(zoom), new ResizeTool(), get(brush)], 36036 fixed_canvas: fixed_canvas(), 36037 border_region: border_region(), 36038 layer_options: layer_options(), 36039 theme_mode: theme_mode() 36040 })); 36041 36042 get(brush).on("change", () => { 36043 has_drawn(true); 36044 }); 36045 36046 set(crop_zoom, new ZoomTool()); 36047 36048 crop = new ImageEditor({ 36049 target_element: get(pixi_target_crop), 36050 width: canvas_size()[0], 36051 height: canvas_size()[1], 36052 tools: ["image", get(crop_zoom), new CropTool()], 36053 dark: true, 36054 fixed_canvas: false, 36055 border_region: 0, 36056 pad_bottom: 40 36057 }); 36058 36059 get(editor).scale.subscribe((_scale) => { 36060 set(zoom_level, _scale); 36061 }); 36062 36063 get(editor).min_zoom.subscribe((is_min_zoom) => { 36064 set(min_zoom, is_min_zoom); 36065 }); 36066 36067 get(editor).dimensions.subscribe((dimensions) => { 36068 last_dimensions = { ...dimensions }; 36069 }); 36070 36071 get(editor).command_manager.current_history.subscribe((history) => { 36072 can_undo(history.previous !== null); 36073 set(can_redo, history.next !== null); 36074 }); 36075 36076 await Promise.all([get(editor).ready, crop.ready]).then(() => { 36077 handle_tool_change({ tool: "image" }); 36078 set(ready, true); 36079 36080 if (sources().length > 0) { 36081 handle_tool_change({ tool: "image" }); 36082 } else { 36083 handle_tool_change({ tool: "draw" }); 36084 } 36085 36086 crop.set_subtool("crop"); 36087 }); 36088 36089 get(editor).on("change", () => { 36090 dispatch("change"); 36091 dispatch("input"); 36092 full_history(get(editor).command_manager.history); 36093 }); 36094 36095 if (background() || layers().length > 0) { 36096 if (background()) { 36097 await add_image_from_url(background()); 36098 } 36099 36100 if (layers().length > 0) { 36101 await add_layers_from_url(layers()); 36102 } 36103 36104 handle_tool_change({ tool: "draw" }); 36105 } else if (composite()) { 36106 await add_image_from_url(composite()); 36107 handle_tool_change({ tool: "draw" }); 36108 } 36109 36110 refresh_tools(); 36111 } 36112 36113 /** 36114 * Handles file uploads 36115 * @param {File[]} files - The uploaded files 36116 */ 36117 async function handle_files(files) { 36118 if (files == null) return; 36119 if (!sources().includes("upload")) return; 36120 36121 get(editor).reset_canvas(); 36122 36123 const _file = Array.isArray(files) ? files[0] : files; 36124 36125 await get(editor).add_image({ image: _file }); 36126 await crop.add_image({ image: _file }); 36127 crop.reset(); 36128 background_image(true); 36129 get(zoom).set_zoom("fit"); 36130 handle_tool_change({ tool: "draw" }); 36131 dispatch("upload"); 36132 } 36133 36134 /** 36135 * Handles tool change events 36136 * @param {{ tool: ToolbarTool }} param0 - Object containing the selected tool 36137 */ 36138 function handle_tool_change({ tool }) { 36139 get(editor).set_tool(tool);
36140 current_tool(tool); 36141 36142 if (tool === "image") { 36143 crop.set_tool("image"); 36144 crop.set_subtool("crop"); 36145 } 36146 } 36147 36148 /** 36149 * Handles subtool change events 36150 * @param {{ tool: ToolbarTool, subtool: Subtool }} param0 - Object containing the selected tool and subtool 36151 */ 36152 function handle_subtool_change({ tool, subtool }) { 36153 get(editor).set_subtool(subtool); 36154 set(current_subtool, subtool); 36155 36156 if (subtool === null) { 36157 return; 36158 } 36159 36160 if (tool === "draw") { 36161 if (subtool === "size") { 36162 set(brush_size_visible, true); 36163 } else if (subtool === "color") { 36164 set(brush_color_visible, true); 36165 } 36166 } 36167 36168 if (tool === "erase" && subtool === "size") { 36169 set(eraser_size_visible, true); 36170 } 36171 36172 if (tool === "image" && subtool === "paste") { 36173 process_clipboard(); 36174 } 36175 36176 if (tool === "image" && subtool === "upload") { 36177 tick().then(() => { 36178 set(disable_click, false); 36179 open_file_upload(); 36180 }); 36181 } 36182 } 36183 36184 let eraser_size_visible = mutable_source(false); 36185 let selected_color = mutable_source(); 36186 let selected_size = mutable_source(); 36187 let selected_opacity = mutable_source(1); 36188 let selected_eraser_size = mutable_source(); 36189 36190 function update_brush_options() { 36191 const default_color = brush_options().default_color === "auto" 36192 ? brush_options().colors[0] 36193 : brush_options().default_color; 36194 36195 // color is already a tuple [color, opacity] 36196 if (Array.isArray(default_color)) { 36197 set(selected_color, default_color[0]); 36198 set(selected_opacity, default_color[1]); 36199 } else { 36200 set(selected_color, default_color); 36201 36202 // color is a string, check if it has opacity info 36203 const color = tinycolor(default_color); 36204 36205 if (color.getAlpha() < 1) { 36206 set(selected_opacity, color.getAlpha()); 36207 } else { 36208 set(selected_opacity, 1); 36209 } 36210 } 36211 36212 set(selected_size, typeof brush_options().default_size === "number" ? brush_options().default_size : 25); 36213 } 36214 36215 function update_eraser_options() { 36216 set(selected_eraser_size, eraser_options().default_size === "auto" ? 25 : eraser_options().default_size); 36217 } 36218 36219 let brush_size_visible = mutable_source(false); 36220 let brush_color_visible = mutable_source(false); 36221 let current_subtool = mutable_source(null); 36222 let preview = mutable_source(false); 36223 36224 function handle_zoom_change(zoom_level) { 36225 get(zoom).set_zoom(zoom_level); 36226 } 36227 36228 function zoom_in_out(direction) { 36229 get(zoom).set_zoom(direction === "in" 36230 ? get(zoom_level) + (get(zoom_level) < 1 ? 0.1 : get(zoom_level) * 0.1) 36231 : get(zoom_level) - (get(zoom_level) < 1 ? 0.1 : get(zoom_level) * 0.1)); 36232 } 36233 36234 async function process_clipboard() { 36235 const items = await navigator.clipboard.read(); 36236 36237 for (let i = 0; i < items.length; i++) { 36238 const type = items[i].types.find((t) => t.startsWith("image/")); 36239 36240 if (type) { 36241 const blob = await items[i].getType(type); 36242 36243 handle_files(blob); 36244 } 36245 } 36246 } 36247 36248 function handle_capture(e) { 36249 if (e.detail !== null) { 36250 handle_files(e.detail); 36251 } 36252 36253 handle_subtool_change({ tool: current_tool(), subtool: null }); 36254 } 36255 36256 function handle_save() { 36257 dispatch("save"); 36258 } 36259 36260 async function handle_crop_confirm() { 36261 const { image } = await crop.get_crop_bounds(); 36262 36263 if (!image) return; 36264 36265 await get(editor).add_image({ image, resize: false }); 36266 handle_subtool_change({ tool: "image", subtool: null }); 36267 dispatch("change"); 36268 dispatch("input"); 36269 } 36270 36271 async function handle_download() { 36272 const blobs = await get(editor).get_blobs(); 36273 const blob = blobs.composite; 36274 36275 if (!blob) { 36276 dispatch("download_error", "Unable to generate image to download."); 36277 36278 return; 36279 } 36280 36281 const url = URL.createObjectURL(blob); 36282 const link = document.createElement("a"); 36283 36284 link.href = url; 36285 link.download = "image.png"; 36286 link.click(); 36287 URL.revokeObjectURL(url); 36288 } 36289 36290 function handle_undo() { 36291 get(editor).undo(); 36292 } 36293 36294 function handle_redo() { 36295 get(editor).redo(); 36296 } 36297 36298 let old_background = mutable_source(background()); 36299 36300 legacy_pre_effect(() => (deep_read_state(layer_options()), get(editor)), () => { 36301 if (layer_options()) { 36302 if (check_if_should_init()) { 36303 get(editor).set_layer_options(layer_options()); 36304 refresh_tools(); 36305 } 36306 } 36307 }); 36308 36309 legacy_pre_effect(() => (get(editor), get(ready), get$1), () => { 36310 if (get(editor) && get(ready) && get(editor).layers) { 36311 const current_layers = get$1(get(editor).layers); 36312 36313 if (current_layers.layers.length > 0 && !current_layers.active_layer) { 36314 refresh_tools_for_layer_changes(current_layers); 36315 } 36316 } 36317 }); 36318 36319 legacy_pre_effect( 36320 () => ( 36321 deep_read_state(background()), 36322 deep_read_state(layers()), 36323 deep_read_state(composite()), 36324 get(editor), 36325 get(ready), 36326 get(zoom) 36327 ), 36328 () => { 36329 if (background() == null && layers().length == 0 && composite() == null && get(editor) && get(ready)) { 36330 get(editor).reset_canvas(); 36331 get(zoom).set_zoom("fit"); 36332 handle_tool_change({ tool: "image" }); 36333 background_image(false); 36334 has_drawn(false); 36335 } 36336 } 36337 ); 36338 36339 legacy_pre_effect(() => (get(old_background), deep_read_state(background())), () => { 36340 if (get(old_background) !== background()) { 36341 handle_tool_change({ tool: "draw" }); 36342 handle_subtool_change({ tool: "draw", subtool: null }); 36343 current_tool("draw"); 36344 set(old_background, background()); 36345 } 36346 }); 36347 36348 legacy_pre_effect( 36349 () => ( 36350 deep_read_state(current_tool()), 36351 get(current_subtool), 36352 get(crop_zoom) 36353 ), 36354 () => { 36355 current_tool() === "image" && get(current_subtool) === "crop" && get(crop_zoom).set_zoom("fit"); 36356 } 36357 ); 36358 36359 legacy_pre_effect(() => (deep_read_state(can_undo()), get(editor)), () => { 36360 background_image(can_undo() && get(editor).command_manager.contains("AddImage")); 36361 }); 36362 36363 legacy_pre_effect( 36364 () => ( 36365 deep_read_state(brush_options()), 36366 deep_read_state(eraser_options()) 36367 ), 36368 () => { 36369 if (brush_options()) { 36370 update_brush_options(); 36371 } 36372 36373 if (eraser_options()) { 36374 update_eraser_options(); 36375 } 36376 } 36377 ); 36378 36379 legacy_pre_effect( 36380 () => ( 36381 get(brush), 36382 get(selected_color), 36383 deep_read_state(brush_options()) 36384 ), 36385 () => { 36386 get(brush)?.set_brush_color((() => { 36387 let color_value; 36388 36389 if (get(selected_color) === "auto") { 36390 const default_color = brush_options().colors.find((color) => Array.isArray(color) 36391 ? color[0] === brush_options().default_color 36392 : color === brush_options().default_color) || brush_options().colors[0]; 36393 36394 color_value = Array.isArray(default_color) ? default_color[0] : default_color; 36395 } else { 36396 color_value = get(selected_color); 36397 } 36398 36399 return color_value; 36400 })()); 36401 } 36402 ); 36403 36404 legacy_pre_effect(() => (get(brush), get(selected_size)), () => { 36405 get(brush)?.set_brush_size(typeof get(selected_size) === "number" ? get(selected_size) : 25); 36406 }); 36407 36408 legacy_pre_effect(() => (get(brush), get(selected_eraser_size)), () => { 36409 get(brush)?.set_eraser_size(typeof get(selected_eraser_size) === "number" ? get(selected_eraser_size) : 25); 36410 }); 36411 36412 legacy_pre_effect( 36413 () => ( 36414 deep_read_state(current_tool()), 36415 deep_read_state(background_image()), 36416 get(current_subtool), 36417 deep_read_state(sources()) 36418 ), 36419 () => { 36420 set(disable_click, current_tool() !== "image" || current_tool() === "image" && background_image() || current_tool() === "image" && get(current_subtool) === "webcam" || !sources().includes("upload")); 36421 } 36422 ); 36423 36424 legacy_pre_effect(() => (get(brush), get(preview)), () => { 36425 get(brush)?.preview_brush(get(preview)); 36426 }); 36427 36428 legacy_pre_effect(() => (get(brush), get(selected_opacity)), () => { 36429 get(brush)?.set_brush_opacity(get(selected_opacity)); 36430 }); 36431 36432 legacy_pre_effect( 36433 () => ( 36434 deep_read_state(composite()), 36435 deep_read_state(background()) 36436 ), 36437 () => { 36438 add_image_from_url(composite() || background()); 36439 } 36440 ); 36441 36442 legacy_pre_effect(() => (deep_read_state(layers())), () => { 36443 add_layers_from_url(layers()); 36444 }); 36445 36446 legacy_pre_effect_reset(); 36447 36448 var $$exports = { 36449 antialias, 36450 get_blobs, 36451 add_image, 36452 add_image_from_url, 36453 add_layers_from_url, 36454 get changeable() { 36455 return changeable(); 36456 }, 36457 36458 set changeable($$value) { 36459 changeable($$value); 36460 flushSync(); 36461 }, 36462 36463 get sources() { 36464 return sources(); 36465 }, 36466 36467 set sources($$value) { 36468 sources($$value); 36469 flushSync(); 36470 }, 36471 36472 get transforms() { 36473 return transforms(); 36474 }, 36475 36476 set transforms($$value) { 36477 transforms($$value); 36478 flushSync(); 36479 }, 36480 36481 get canvas_size() { 36482 return canvas_size(); 36483 }, 36484 36485 set canvas_size($$value) { 36486 canvas_size($$value); 36487 flushSync(); 36488 }, 36489 36490 get is_dragging() { 36491 return is_dragging(); 36492 }, 36493 36494 set is_dragging($$value) { 36495 is_dragging($$value); 36496 flushSync(); 36497 }, 36498 36499 get background_image() { 36500 return background_image(); 36501 }, 36502 36503 set background_image($$value) { 36504 background_image($$value); 36505 flushSync(); 36506 }, 36507 36508 get brush_options() { 36509 return brush_options(); 36510 }, 36511 36512 set brush_options($$value) { 36513 brush_options($$value); 36514 flushSync(); 36515 }, 36516 36517 get eraser_options() { 36518 return eraser_options(); 36519 }, 36520 36521 set eraser_options($$value) { 36522 eraser_options($$value); 36523 flushSync(); 36524 }, 36525 36526 get fixed_canvas() { 36527 return fixed_canvas(); 36528 }, 36529 36530 set fixed_canvas($$value) { 36531 fixed_canvas($$value); 36532 flushSync(); 36533 }, 36534 36535 get root() {
36536 return root(); 36537 }, 36538 36539 set root($$value) { 36540 root($$value); 36541 flushSync(); 36542 }, 36543 36544 get i18n() { 36545 return i18n(); 36546 }, 36547 36548 set i18n($$value) { 36549 i18n($$value); 36550 flushSync(); 36551 }, 36552 36553 get upload() { 36554 return upload(); 36555 }, 36556 36557 set upload($$value) { 36558 upload($$value); 36559 flushSync(); 36560 }, 36561 36562 get composite() { 36563 return composite(); 36564 }, 36565 36566 set composite($$value) { 36567 composite($$value); 36568 flushSync(); 36569 }, 36570 36571 get layers() { 36572 return layers(); 36573 }, 36574 36575 set layers($$value) { 36576 layers($$value); 36577 flushSync(); 36578 }, 36579 36580 get background() { 36581 return background(); 36582 }, 36583 36584 set background($$value) { 36585 background($$value); 36586 flushSync(); 36587 }, 36588 36589 get border_region() { 36590 return border_region(); 36591 }, 36592 36593 set border_region($$value) { 36594 border_region($$value); 36595 flushSync(); 36596 }, 36597 36598 get layer_options() { 36599 return layer_options(); 36600 }, 36601 36602 set layer_options($$value) { 36603 layer_options($$value); 36604 flushSync(); 36605 }, 36606 36607 get current_tool() { 36608 return current_tool(); 36609 }, 36610 36611 set current_tool($$value) { 36612 current_tool($$value); 36613 flushSync(); 36614 }, 36615 36616 get webcam_options() { 36617 return webcam_options(); 36618 }, 36619 36620 set webcam_options($$value) { 36621 webcam_options($$value); 36622 flushSync(); 36623 }, 36624 36625 get show_download_button() { 36626 return show_download_button(); 36627 }, 36628 36629 set show_download_button($$value) { 36630 show_download_button($$value); 36631 flushSync(); 36632 }, 36633 36634 get theme_mode() { 36635 return theme_mode(); 36636 }, 36637 36638 set theme_mode($$value) { 36639 theme_mode($$value); 36640 flushSync(); 36641 }, 36642 36643 get full_history() { 36644 return full_history(); 36645 }, 36646 36647 set full_history($$value) { 36648 full_history($$value); 36649 flushSync(); 36650 }, 36651 36652 get has_drawn() { 36653 return has_drawn(); 36654 }, 36655 36656 set has_drawn($$value) { 36657 has_drawn($$value); 36658 flushSync(); 36659 }, 36660 36661 get can_undo() { 36662 return can_undo(); 36663 }, 36664 36665 set can_undo($$value) { 36666 can_undo($$value); 36667 flushSync(); 36668 } 36669 }; 36670 36671 init$1(); 36672 36673 var div = root_1$1(); 36674 let classes; 36675 36676 set_attribute(div, 'aria-label', "Click to upload or drop files"); 36677 36678 var node = child(div); 36679 36680 { 36681 var consequent_4 = ($$anchor) => { 36682 var fragment = root_2$1(); 36683 var node_1 = first_child(fragment); 36684 36685 { 36686 var consequent = ($$anchor) => { 36687 Controls($$anchor, { 36688 get changeable() { 36689 return changeable(); 36690 }, 36691 36692 get min_zoom() { 36693 return get(min_zoom); 36694 }, 36695 36696 get current_zoom() { 36697 return get(zoom_level); 36698 }, 36699 36700 get tool() { 36701 return current_tool(); 36702 }, 36703 can_save: true, 36704 get enable_download() { 36705 return show_download_button(); 36706 }, 36707 36708 get can_undo() { 36709 return can_undo(); 36710 }, 36711 36712 get can_redo() { 36713 return get(can_redo); 36714 }, 36715 36716 $$events: { 36717 set_zoom: (e) => handle_zoom_change(e.detail), 36718 zoom_in: () => zoom_in_out("in"), 36719 zoom_out: () => zoom_in_out("out"), 36720 remove_image: () => { 36721 dispatch("clear"); 36722 get(editor).reset_canvas(); 36723 handle_tool_change({ tool: "image" }); 36724 background_image(false); 36725 has_drawn(false); 36726 }, 36727 save: handle_save, 36728 pan: (e) => { 36729 handle_tool_change({ tool: "pan" }); 36730 }, 36731 download: () => handle_download(), 36732 undo: handle_undo, 36733 redo: handle_redo 36734 } 36735 }); 36736 }; 36737 36738 if_block(node_1, ($$render) => { 36739 if (get(current_subtool) !== "crop") $$render(consequent); 36740 }); 36741 } 36742 36743 var node_2 = sibling(node_1, 2); 36744 36745 { 36746 var consequent_1 = ($$anchor) => { 36747 Toolbar($$anchor, { 36748 get sources() { 36749 return sources(); 36750 }, 36751 36752 get transforms() { 36753 return transforms(); 36754 }, 36755 36756 get background() { 36757 return background_image(); 36758 }, 36759 36760 get show_brush_size() { 36761 return get(brush_size_visible); 36762 }, 36763 36764 get show_brush_color() { 36765 return get(brush_color_visible); 36766 }, 36767 36768 get show_eraser_size() { 36769 return get(eraser_size_visible); 36770 }, 36771 36772 get brush_options() { 36773 return brush_options(); 36774 }, 36775 36776 get eraser_options() { 36777 return eraser_options(); 36778 }, 36779 36780 get tool() { 36781 return current_tool(); 36782 }, 36783 36784 get subtool() { 36785 return get(current_subtool); 36786 }, 36787 36788 get selected_color() { 36789 return get(selected_color); 36790 }, 36791 36792 set selected_color($$value) { 36793 set(selected_color, $$value); 36794 }, 36795 36796 get selected_size() { 36797 return get(selected_size); 36798 }, 36799 36800 set selected_size($$value) { 36801 set(selected_size, $$value); 36802 }, 36803 36804 get selected_eraser_size() { 36805 return get(selected_eraser_size); 36806 }, 36807 36808 set selected_eraser_size($$value) { 36809 set(selected_eraser_size, $$value); 36810 }, 36811 36812 get selected_opacity() { 36813 return get(selected_opacity); 36814 }, 36815 36816 set selected_opacity($$value) { 36817 set(selected_opacity, $$value); 36818 }, 36819 36820 get preview() { 36821 return get(preview); 36822 }, 36823 36824 set preview($$value) { 36825 set(preview, $$value); 36826 }, 36827 36828 $$events: { 36829 tool_change: (e) => handle_tool_change(e.detail), 36830 subtool_change: (e) => handle_subtool_change(e.detail) 36831 }, 36832 $$legacy: true 36833 }); 36834 }; 36835 36836 if_block(node_2, ($$render) => { 36837 if (get(current_subtool) !== "crop") $$render(consequent_1); 36838 }); 36839 } 36840 36841 var node_3 = sibling(node_2, 2); 36842 36843 { 36844 var consequent_2 = ($$anchor) => { 36845 var div_1 = root_5$1(); 36846 var div_2 = child(div_1); 36847 var node_4 = child(div_2); 36848 36849 Webcam$1(node_4, { 36850 get upload() { 36851 return upload(); 36852 }, 36853 36854 get root() {
36855 return root(); 36856 }, 36857 streaming: false, 36858 mode: 'image', 36859 include_audio: false, 36860 get i18n() { 36861 return i18n(); 36862 }, 36863 36864 get mirror_webcam() { 36865 return ( 36866 deep_read_state(webcam_options()), 36867 untrack(() => webcam_options().mirror) 36868 ); 36869 }, 36870 36871 get webcam_constraints() { 36872 return ( 36873 deep_read_state(webcam_options()), 36874 untrack(() => webcam_options().constraints) 36875 ); 36876 }, 36877 36878 $$events: { 36879 capture: handle_capture, 36880 error($$arg) { 36881 bubble_event.call(this, $$props, $$arg); 36882 }, 36883 36884 drag($$arg) { 36885 bubble_event.call(this, $$props, $$arg); 36886 } 36887 } 36888 }); 36889 36890 reset(div_2); 36891 reset(div_1); 36892 append($$anchor, div_1); 36893 }; 36894 36895 if_block(node_3, ($$render) => { 36896 if (current_tool() === "image" && get(current_subtool) === "webcam") $$render(consequent_2); 36897 }); 36898 } 36899 36900 var node_5 = sibling(node_3, 2); 36901 36902 { 36903 var consequent_3 = ($$anchor) => { 36904 SecondaryToolbar($$anchor, { 36905 get enable_additional_layers() { 36906 return ( 36907 deep_read_state(layer_options()), 36908 untrack(() => layer_options().allow_additional_layers) 36909 ); 36910 }, 36911 36912 get layers() { 36913 return (get(editor), untrack(() => get(editor).layers)); 36914 }, 36915 36916 $$events: { 36917 new_layer: () => { 36918 get(editor).add_layer(); 36919 }, 36920 36921 change_layer: (e) => { 36922 get(editor).set_layer(e.detail); 36923 36924 if (current_tool() === "draw") { 36925 handle_tool_change({ tool: "draw" }); 36926 } 36927 }, 36928 36929 move_layer: (e) => { 36930 get(editor).move_layer(e.detail.id, e.detail.direction); 36931 }, 36932 36933 delete_layer: (e) => { 36934 get(editor).delete_layer(e.detail); 36935 }, 36936 36937 toggle_layer_visibility: (e) => { 36938 get(editor).toggle_layer_visibility(e.detail); 36939 } 36940 } 36941 }); 36942 }; 36943 36944 if_block(node_5, ($$render) => { 36945 if (( 36946 get(current_subtool), 36947 deep_read_state(layer_options()), 36948 untrack(() => get(current_subtool) !== "crop" && !layer_options().disabled) 36949 )) $$render(consequent_3); 36950 }); 36951 } 36952 36953 append($$anchor, fragment); 36954 }; 36955 36956 if_block(node, ($$render) => { 36957 if (get(ready)) $$render(consequent_4); 36958 }); 36959 } 36960 36961 var div_3 = sibling(node, 2); 36962 let classes_1; 36963 36964 bind_this(div_3, ($$value) => set(pixi_target, $$value), () => get(pixi_target)); 36965 36966 var div_4 = sibling(div_3, 2); 36967 let classes_2; 36968 36969 bind_this(div_4, ($$value) => set(pixi_target_crop, $$value), () => get(pixi_target_crop)); 36970 36971 var node_6 = sibling(div_4, 2); 36972 36973 { 36974 var consequent_5 = ($$anchor) => { 36975 var div_5 = root_7$1(); 36976 var node_7 = child(div_5); 36977 36978 IconButton(node_7, { 36979 get Icon() { 36980 return Check; 36981 }, 36982 label: 'Confirm crop', 36983 show_label: true, 36984 size: 'large', 36985 padded: true, 36986 color: 'white', 36987 background: 'var(--color-green-500)', 36988 label_position: 'right', 36989 onclick: handle_crop_confirm 36990 }); 36991 36992 reset(div_5); 36993 append($$anchor, div_5); 36994 }; 36995 36996 if_block(node_6, ($$render) => { 36997 if (get(current_subtool) === "crop") $$render(consequent_5); 36998 }); 36999 } 37000 37001 var node_8 = sibling(node_6, 2); 37002 37003 slot(node_8, $$props, 'default', {}, null); 37004 reset(div); 37005 37006 action(div, ($$node, $$action_arg) => drag?.($$node, $$action_arg), () => ({ 37007 on_drag_change: (dragging) => is_dragging(dragging), 37008 on_files: handle_files, 37009 accepted_types: "image/*", 37010 disable_click: get(disable_click) 37011 })); 37012 37013 template_effect(() => { 37014 classes = set_class(div, 1, 'image-container svelte-6fnl1z', null, classes, { 'dark-bg': get(current_subtool) === "crop" }); 37015 classes_1 = set_class(div_3, 1, 'pixi-target svelte-6fnl1z', null, classes_1, { visible: get(current_subtool) !== "crop" }); 37016 classes_2 = set_class(div_4, 1, 'pixi-target-crop svelte-6fnl1z', null, classes_2, { visible: get(current_subtool) === "crop" }); 37017 }); 37018 37019 append($$anchor, div); 37020 bind_prop($$props, 'antialias', antialias); 37021 bind_prop($$props, 'get_blobs', get_blobs); 37022 bind_prop($$props, 'add_image', add_image); 37023 bind_prop($$props, 'add_image_from_url', add_image_from_url); 37024 bind_prop($$props, 'add_layers_from_url', add_layers_from_url); 37025 37026 return pop($$exports); 37027} 37028 37029const RE_HEADING = /^(#\s*)(.+)$/m; 37030function inject(text) { 37031 const trimmed_text = text.trim(); 37032 const heading_match = trimmed_text.match(RE_HEADING); 37033 if (!heading_match) { 37034 return [false, trimmed_text || false]; 37035 } 37036 const [full_match, , heading_content] = heading_match; 37037 const _heading = heading_content.trim(); 37038 if (trimmed_text === full_match) { 37039 return [_heading, false]; 37040 } 37041 const heading_end_index = heading_match.index !== void 0 ? heading_match.index + full_match.length : 0; 37042 const remaining_text = trimmed_text.substring(heading_end_index).trim(); 37043 const _paragraph = remaining_text || false; 37044 return [_heading, _paragraph]; 37045} 37046 37047var root_6 = from_html(`<h2 class="svelte-b4yllx"> </h2>`); 37048var root_7 = from_html(`<p class="svelte-b4yllx"> </p>`); 37049var root_5 = from_html(`<!> <!>`, 1); 37050var root_8 = from_html(`<div>Upload an image</div>`); 37051var root_9 = from_html(`<div class="or svelte-b4yllx">or</div>`); 37052var root_10 = from_html(`<div>select the draw tool to start</div>`); 37053var root_3 = from_html(`<div class="empty wrap svelte-b4yllx"><!> <!> <!></div>`); 37054var root_1 = from_html(`<!> <!>`, 1); 37055 37056function InteractiveImageEditor($$anchor, $$props) { 37057 push($$props, false); 37058 37059 const heading = mutable_source(); 37060 const paragraph = mutable_source(); 37061 let brush = prop($$props, 'brush', 12); 37062 let eraser = prop($$props, 'eraser', 12); 37063 let sources = prop($$props, 'sources', 12); 37064 let i18n = prop($$props, 'i18n', 12); 37065 let root = prop($$props, 'root', 12); 37066 let label = prop($$props, 'label', 12, undefined); 37067 let show_label = prop($$props, 'show_label', 12); 37068 let changeable = prop($$props, 'changeable', 12, false); 37069 let theme_mode = prop($$props, 'theme_mode', 12); 37070 let layers = prop($$props, 'layers', 12); 37071 let composite = prop($$props, 'composite', 12); 37072 let background = prop($$props, 'background', 12); 37073 let layer_options = prop($$props, 'layer_options', 12); 37074 let transforms = prop($$props, 'transforms', 12); 37075 let accept_blobs = prop($$props, 'accept_blobs', 12); 37076 let canvas_size = prop($$props, 'canvas_size', 12); 37077 let fixed_canvas = prop($$props, 'fixed_canvas', 12, false); 37078 let realtime = prop($$props, 'realtime', 12); 37079 let upload = prop($$props, 'upload', 12); 37080 let is_dragging = prop($$props, 'is_dragging', 12);
37081 let placeholder = prop($$props, 'placeholder', 12, undefined); 37082 let border_region = prop($$props, 'border_region', 12); 37083 let full_history = prop($$props, 'full_history', 12, null); 37084 let webcam_options = prop($$props, 'webcam_options', 12); 37085 let show_download_button = prop($$props, 'show_download_button', 12, false); 37086 const dispatch = createEventDispatcher(); 37087 let editor = mutable_source(); 37088 let has_drawn = mutable_source(false); 37089 37090 function is_not_null(o) { 37091 return !!o; 37092 } 37093 37094 function is_file_data(o) { 37095 return !!o; 37096 } 37097 37098 async function get_data() { 37099 let blobs; 37100 37101 try { 37102 blobs = await get(editor).get_blobs(); 37103 } catch(e) { 37104 return { background: null, layers: [], composite: null }; 37105 } 37106 37107 const bg = blobs.background 37108 ? upload()(await prepare_files([new File([blobs.background], "background.png")]), root()) 37109 : Promise.resolve(null); 37110 37111 const layers = blobs.layers.filter(is_not_null).map(async (blob, i) => upload()(await prepare_files([new File([blob], `layer_${i}.png`)]), root())); 37112 37113 const composite = blobs.composite 37114 ? upload()(await prepare_files([new File([blobs.composite], "composite.png")]), root()) 37115 : Promise.resolve(null); 37116 37117 const [background, composite_, ...layers_] = await Promise.all([bg, composite, ...layers]); 37118 37119 return { 37120 background: Array.isArray(background) ? background[0] : background, 37121 layers: layers_.flatMap((layer) => Array.isArray(layer) ? layer : [layer]).filter(is_file_data), 37122 composite: Array.isArray(composite_) ? composite_[0] : composite_ 37123 }; 37124 } 37125 37126 function handle_value(value) { 37127 if (!get(editor)) return; 37128 37129 if (value == null) { 37130 get(editor).handle_remove(); 37131 dispatch("receive_null"); 37132 } 37133 } 37134 37135 let background_image = mutable_source(false); 37136 let can_undo = mutable_source(); 37137 let image_id = prop($$props, 'image_id', 12, null); 37138 37139 function nextframe() { 37140 return new Promise((resolve) => setTimeout(() => resolve(), 30)); 37141 } 37142 37143 let uploading = false; 37144 let pending = false; 37145 37146 async function handle_change(e) { 37147 if (!realtime()) return; 37148 37149 if (uploading) { 37150 pending = true; 37151 37152 return; 37153 } 37154 37155 uploading = true; 37156 await nextframe(); 37157 37158 const blobs = await get(editor).get_blobs(); 37159 const images = []; 37160 let id = Math.random().toString(36).substring(2); 37161 37162 if (blobs.background) images.push([ 37163 id, 37164 "background", 37165 new File([blobs.background], "background.png"), 37166 null 37167 ]); 37168 37169 if (blobs.composite) images.push([ 37170 id, 37171 "composite", 37172 new File([blobs.composite], "composite.png"), 37173 null 37174 ]); 37175 37176 blobs.layers.forEach((layer, i) => { 37177 if (layer) images.push([id, `layer`, new File([layer], `layer_${i}.png`), i]); 37178 }); 37179 37180 await Promise.all(images.map(async ([image_id, type, data, index]) => { 37181 return accept_blobs()({ 37182 binary: true, 37183 data: { file: data, id: image_id, type, index } 37184 }); 37185 })); 37186 37187 image_id(id); 37188 dispatch("change"); 37189 await nextframe(); 37190 uploading = false; 37191 37192 if (pending) { 37193 pending = false; 37194 uploading = false; 37195 handle_change(); 37196 } 37197 } 37198 37199 let current_tool = mutable_source(); 37200 37201 legacy_pre_effect(() => (get(background_image)), () => { 37202 if (get(background_image)) dispatch("upload"); 37203 }); 37204 37205 legacy_pre_effect( 37206 () => ( 37207 deep_read_state(layers()), 37208 deep_read_state(composite()), 37209 deep_read_state(background()) 37210 ), 37211 () => { 37212 handle_value({ 37213 layers: layers(), 37214 composite: composite(), 37215 background: background() 37216 }); 37217 } 37218 ); 37219 37220 legacy_pre_effect( 37221 () => ( 37222 get(heading), 37223 get(paragraph), 37224 deep_read_state(placeholder()), 37225 inject 37226 ), 37227 () => { 37228 (($$value) => { 37229 var $$array = to_array($$value, 2); 37230 37231 set(heading, $$array[0]); 37232 set(paragraph, $$array[1]); 37233 })(placeholder() ? inject(placeholder()) : [false, false]); 37234 } 37235 ); 37236 37237 legacy_pre_effect_reset(); 37238 37239 var $$exports = { 37240 get_data, 37241 get brush() { 37242 return brush(); 37243 }, 37244 37245 set brush($$value) { 37246 brush($$value); 37247 flushSync(); 37248 }, 37249 37250 get eraser() { 37251 return eraser(); 37252 }, 37253 37254 set eraser($$value) { 37255 eraser($$value); 37256 flushSync(); 37257 }, 37258 37259 get sources() { 37260 return sources(); 37261 }, 37262 37263 set sources($$value) { 37264 sources($$value); 37265 flushSync(); 37266 }, 37267 37268 get i18n() { 37269 return i18n(); 37270 }, 37271 37272 set i18n($$value) { 37273 i18n($$value); 37274 flushSync(); 37275 }, 37276 37277 get root() {
37278 return root(); 37279 }, 37280 37281 set root($$value) { 37282 root($$value); 37283 flushSync(); 37284 }, 37285 37286 get label() { 37287 return label(); 37288 }, 37289 37290 set label($$value) { 37291 label($$value); 37292 flushSync(); 37293 }, 37294 37295 get show_label() { 37296 return show_label(); 37297 }, 37298 37299 set show_label($$value) { 37300 show_label($$value); 37301 flushSync(); 37302 }, 37303 37304 get changeable() { 37305 return changeable(); 37306 }, 37307 37308 set changeable($$value) { 37309 changeable($$value); 37310 flushSync(); 37311 }, 37312 37313 get theme_mode() { 37314 return theme_mode(); 37315 }, 37316 37317 set theme_mode($$value) { 37318 theme_mode($$value); 37319 flushSync(); 37320 }, 37321 37322 get layers() { 37323 return layers(); 37324 }, 37325 37326 set layers($$value) { 37327 layers($$value); 37328 flushSync(); 37329 }, 37330 37331 get composite() { 37332 return composite(); 37333 }, 37334 37335 set composite($$value) { 37336 composite($$value); 37337 flushSync(); 37338 }, 37339 37340 get background() { 37341 return background(); 37342 }, 37343 37344 set background($$value) { 37345 background($$value); 37346 flushSync(); 37347 }, 37348 37349 get layer_options() { 37350 return layer_options(); 37351 }, 37352 37353 set layer_options($$value) { 37354 layer_options($$value); 37355 flushSync(); 37356 }, 37357 37358 get transforms() { 37359 return transforms(); 37360 }, 37361 37362 set transforms($$value) { 37363 transforms($$value); 37364 flushSync(); 37365 }, 37366 37367 get accept_blobs() { 37368 return accept_blobs(); 37369 }, 37370 37371 set accept_blobs($$value) { 37372 accept_blobs($$value); 37373 flushSync(); 37374 }, 37375 37376 get canvas_size() { 37377 return canvas_size(); 37378 }, 37379 37380 set canvas_size($$value) { 37381 canvas_size($$value); 37382 flushSync(); 37383 }, 37384 37385 get fixed_canvas() { 37386 return fixed_canvas(); 37387 }, 37388 37389 set fixed_canvas($$value) { 37390 fixed_canvas($$value); 37391 flushSync(); 37392 }, 37393 37394 get realtime() { 37395 return realtime(); 37396 }, 37397 37398 set realtime($$value) { 37399 realtime($$value); 37400 flushSync(); 37401 }, 37402 37403 get upload() { 37404 return upload(); 37405 }, 37406 37407 set upload($$value) { 37408 upload($$value); 37409 flushSync(); 37410 }, 37411 37412 get is_dragging() { 37413 return is_dragging(); 37414 }, 37415 37416 set is_dragging($$value) { 37417 is_dragging($$value); 37418 flushSync(); 37419 }, 37420 37421 get placeholder() { 37422 return placeholder(); 37423 }, 37424 37425 set placeholder($$value) { 37426 placeholder($$value); 37427 flushSync(); 37428 }, 37429 37430 get border_region() { 37431 return border_region(); 37432 }, 37433 37434 set border_region($$value) { 37435 border_region($$value); 37436 flushSync(); 37437 }, 37438 37439 get full_history() { 37440 return full_history(); 37441 }, 37442 37443 set full_history($$value) { 37444 full_history($$value); 37445 flushSync(); 37446 }, 37447 37448 get webcam_options() { 37449 return webcam_options(); 37450 }, 37451 37452 set webcam_options($$value) { 37453 webcam_options($$value); 37454 flushSync(); 37455 }, 37456 37457 get show_download_button() { 37458 return show_download_button(); 37459 }, 37460 37461 set show_download_button($$value) { 37462 show_download_button($$value); 37463 flushSync(); 37464 }, 37465 37466 get image_id() { 37467 return image_id(); 37468 }, 37469 37470 set image_id($$value) { 37471 image_id($$value); 37472 flushSync(); 37473 } 37474 }; 37475 37476 init$1(); 37477 37478 var fragment = root_1(); 37479 var node = first_child(fragment); 37480 37481 { 37482 let $0 = derived_safe_equal(() => ( 37483 deep_read_state(label()), 37484 deep_read_state(i18n()), 37485 untrack(() => label() || i18n()("image.image")) 37486 )); 37487 37488 BlockLabel(node, { 37489 get show_label() { 37490 return show_label(); 37491 }, 37492 37493 get Icon() { 37494 return Image$1; 37495 }, 37496 37497 get label() { 37498 return get($0); 37499 } 37500 }); 37501 } 37502 37503 var node_1 = sibling(node, 2); 37504 37505 bind_this( 37506 ImageEditor_1(node_1, { 37507 get transforms() { 37508 return transforms(); 37509 }, 37510 37511 get composite() { 37512 return composite(); 37513 }, 37514 37515 get layers() { 37516 return layers(); 37517 }, 37518 37519 get background() { 37520 return background(); 37521 }, 37522 37523 get canvas_size() { 37524 return canvas_size(); 37525 }, 37526 37527 get changeable() { 37528 return changeable(); 37529 }, 37530 37531 get sources() { 37532 return sources(); 37533 }, 37534 37535 get brush_options() { 37536 return brush(); 37537 }, 37538 37539 get eraser_options() { 37540 return eraser(); 37541 }, 37542 37543 get fixed_canvas() { 37544 return fixed_canvas(); 37545 }, 37546 37547 get border_region() { 37548 return border_region(); 37549 }, 37550 37551 get layer_options() { 37552 return layer_options(); 37553 }, 37554 37555 get i18n() { 37556 return i18n(); 37557 }, 37558 37559 get root() { 37560 return root(); 37561 }, 37562 37563 get upload() { 37564 return upload(); 37565 }, 37566 37567 get webcam_options() { 37568 return webcam_options(); 37569 }, 37570 37571 get show_download_button() { 37572 return show_download_button(); 37573 }, 37574 37575 get theme_mode() { 37576 return theme_mode(); 37577 }, 37578 37579 get background_image() { 37580 return get(background_image); 37581 }, 37582 37583 set background_image($$value) { 37584 set(background_image, $$value); 37585 }, 37586 37587 get current_tool() { 37588 return get(current_tool); 37589 }, 37590 37591 set current_tool($$value) { 37592 set(current_tool, $$value); 37593 }, 37594 37595 get is_dragging() { 37596 return is_dragging(); 37597 }, 37598 37599 set is_dragging($$value) { 37600 is_dragging($$value); 37601 }, 37602 37603 get has_drawn() { 37604 return get(has_drawn); 37605 }, 37606 37607 set has_drawn($$value) { 37608 set(has_drawn, $$value); 37609 }, 37610 37611 get can_undo() { 37612 return get(can_undo); 37613 }, 37614 37615 set can_undo($$value) { 37616 set(can_undo, $$value); 37617 }, 37618 37619 get full_history() { 37620 return full_history(); 37621 }, 37622 37623 set full_history($$value) { 37624 full_history($$value); 37625 }, 37626 37627 $$events: { 37628 history($$arg) { 37629 bubble_event.call(this, $$props, $$arg); 37630 }, 37631 37632 save($$arg) { 37633 bubble_event.call(this, $$props, $$arg); 37634 }, 37635 37636 input($$arg) { 37637 bubble_event.call(this, $$props, $$arg); 37638 }, 37639 change: handle_change, 37640 clear: () => dispatch("clear"), 37641 download_error($$arg) { 37642 bubble_event.call(this, $$props, $$arg); 37643 } 37644 }, 37645 37646 children: ($$anchor, $$slotProps) => { 37647 var fragment_1 = comment(); 37648 var node_2 = first_child(fragment_1); 37649 37650 { 37651 var consequent_6 = ($$anchor) => { 37652 var div = root_3(); 37653 var node_3 = child(div); 37654 37655 { 37656 var consequent_3 = ($$anchor) => { 37657 var fragment_2 = comment(); 37658 var node_4 = first_child(fragment_2); 37659 37660 { 37661 var consequent_2 = ($$anchor) => { 37662 var fragment_3 = root_5(); 37663 var node_5 = first_child(fragment_3); 37664 37665 { 37666 var consequent = ($$anchor) => { 37667 var h2 = root_6(); 37668 var text = child(h2, true); 37669 37670 reset(h2); 37671 template_effect(() => set_text(text, get(heading))); 37672 append($$anchor, h2); 37673 }; 37674 37675 if_block(node_5, ($$render) => { 37676 if (get(heading)) $$render(consequent); 37677 }); 37678 } 37679 37680 var node_6 = sibling(node_5, 2); 37681 37682 { 37683 var consequent_1 = ($$anchor) => { 37684 var p = root_7(); 37685 var text_1 = child(p, true); 37686 37687 reset(p); 37688 template_effect(() => set_text(text_1, get(paragraph))); 37689 append($$anchor, p); 37690 }; 37691 37692 if_block(node_6, ($$render) => { 37693 if (get(paragraph)) $$render(consequent_1); 37694 }); 37695 } 37696 37697 append($$anchor, fragment_3); 37698 }; 37699 37700 var alternate = ($$anchor) => { 37701 var div_1 = root_8(); 37702 37703 append($$anchor, div_1); 37704 }; 37705 37706 if_block(node_4, ($$render) => { 37707 if (get(heading) || get(paragraph)) $$render(consequent_2); else $$render(alternate, false); 37708 }); 37709 } 37710 37711 append($$anchor, fragment_2); 37712 }; 37713 37714 if_block(node_3, ($$render) => { 37715 if (( 37716 deep_read_state(sources()), 37717 untrack(() => sources() && sources().length) 37718 )) $$render(consequent_3); 37719 }); 37720 } 37721 37722 var node_7 = sibling(node_3, 2); 37723 37724 { 37725 var consequent_4 = ($$anchor) => { 37726 var div_2 = root_9(); 37727 37728 append($$anchor, div_2); 37729 }; 37730 37731 if_block(node_7, ($$render) => { 37732 if (( 37733 deep_read_state(sources()), 37734 deep_read_state(brush()), 37735 deep_read_state(placeholder()), 37736 untrack(() => sources() && sources().length && brush() && !placeholder()) 37737 )) $$render(consequent_4); 37738 }); 37739 } 37740 37741 var node_8 = sibling(node_7, 2); 37742 37743 { 37744 var consequent_5 = ($$anchor) => { 37745 var div_3 = root_10(); 37746 37747 append($$anchor, div_3); 37748 }; 37749 37750 if_block(node_8, ($$render) => { 37751 if (brush() && !placeholder()) $$render(consequent_5); 37752 }); 37753 } 37754 37755 reset(div); 37756 append($$anchor, div); 37757 }; 37758 37759 if_block(node_2, ($$render) => { 37760 if (get(current_tool) === "image" && !get(can_undo)) $$render(consequent_6); 37761 }); 37762 } 37763 37764 append($$anchor, fragment_1); 37765 }, 37766 $$slots: { default: true }, 37767 $$legacy: true 37768 }), 37769 ($$value) => set(editor, $$value), 37770 () => get(editor) 37771 ); 37772 37773 append($$anchor, fragment); 37774 bind_prop($$props, 'get_data', get_data); 37775 37776 return pop($$exports); 37777} 37778 37779var root_2 = from_html(`<!> <!>`, 1); 37780var root_4 = from_html(`<!> <!>`, 1); 37781 37782function Index($$anchor, $$props) { 37783 push($$props, true); 37784 37785 let props = rest_props($$props, ['$$slots', '$$events', '$$legacy']); 37786 37787 class ImageEditorGradio extends Gradio { 37788 async get_data() { 37789 const data = await super.get_data(); 37790 const value = await _get_value(); 37791 37792 return { ...data, value }; 37793 } 37794 } 37795 37796 const gradio = new ImageEditorGradio(props, { 37797 server: { accept_blobs: () => {} }, 37798 buttons: [], 37799 height: 350, 37800 border_region: 0 37801 }); 37802 37803 let editor_instance; 37804 let image_id = state(null); 37805 let has_run_change = false;
37806 let has_run_input = false; 37807 37808 async function _get_value() { 37809 if (get(image_id)) { 37810 const val = { id: get(image_id) }; 37811 37812 set(image_id, null); 37813 37814 return val; 37815 } 37816 37817 const blobs = await editor_instance.get_data(); 37818 37819 return blobs; 37820 } 37821 37822 let is_dragging; 37823 37824 // function handle_change(): void { 37825 // if (gradio.props.value) gradio.dispatch("change"); 37826 // } 37827 function handle_save() { 37828 gradio.dispatch("apply"); 37829 } 37830 37831 // function handle_history_change(): void { 37832 // gradio.dispatch("change"); 37833 // gradio.dispatch("input"); 37834 // } 37835 let has_value = user_derived(() => gradio.props.value?.background || gradio.props.value?.layers?.length || gradio.props.value?.composite); 37836 37837 let normalised_background = user_derived(() => gradio.props.value?.background ? new FileData(gradio.props.value.background) : null); 37838 let normalised_composite = user_derived(() => gradio.props.value?.composite ? new FileData(gradio.props.value.composite) : null); 37839 let normalised_layers = user_derived(() => gradio.props.value?.layers?.map((layer) => new FileData(layer)) || []); 37840 var fragment = comment(); 37841 var node = first_child(fragment); 37842 37843 { 37844 var consequent = ($$anchor) => { 37845 { 37846 let $0 = user_derived(() => is_dragging ? "focus" : "base"); 37847 37848 Block($$anchor, { 37849 get visible() { 37850 return gradio.shared.visible; 37851 }, 37852 variant: "solid", 37853 get border_mode() { 37854 return get($0); 37855 }, 37856 padding: false, 37857 get elem_id() { 37858 return gradio.shared.elem_id; 37859 }, 37860 37861 get elem_classes() { 37862 return gradio.shared.elem_classes; 37863 }, 37864 37865 get height() { 37866 return gradio.props.height; 37867 }, 37868 37869 get width() { 37870 return gradio.props.width; 37871 }, 37872 allow_overflow: true, 37873 overflow_behavior: 'visible', 37874 get container() { 37875 return gradio.shared.container; 37876 }, 37877 37878 get scale() { 37879 return gradio.shared.scale; 37880 }, 37881 37882 get min_width() { 37883 return gradio.shared.min_width; 37884 }, 37885 37886 children: ($$anchor, $$slotProps) => { 37887 var fragment_2 = root_2();
37888 var node_1 = first_child(fragment_2); 37889 37890 Static(node_1, spread_props( 37891 { 37892 get autoscroll() { 37893 return gradio.shared.autoscroll; 37894 }, 37895 37896 get i18n() { 37897 return gradio.i18n; 37898 } 37899 }, 37900 () => gradio.shared.loading_status, 37901 { 37902 on_clear_status: () => gradio.dispatch("clear_status", gradio.shared.loading_status) 37903 } 37904 )); 37905 37906 var node_2 = sibling(node_1, 2); 37907 37908 { 37909 let $0 = user_derived(() => gradio.props.value?.composite || null); 37910 37911 ImagePreview(node_2, { 37912 get value() { 37913 return get($0); 37914 }, 37915 37916 get label() { 37917 return gradio.shared.label; 37918 }, 37919 37920 get show_label() { 37921 return gradio.shared.show_label; 37922 }, 37923 37924 get buttons() { 37925 return gradio.props.buttons; 37926 }, 37927 37928 get selectable() { 37929 return gradio.props._selectable; 37930 }, 37931 37932 get i18n() { 37933 return gradio.i18n; 37934 }, 37935 37936 $$events: { 37937 select: ({ detail }) => gradio.dispatch("select", detail), 37938 share: ({ detail }) => gradio.dispatch("share", detail), 37939 error: ({ detail }) => gradio.dispatch("error", detail) 37940 } 37941 }); 37942 } 37943 37944 append($$anchor, fragment_2); 37945 }, 37946 $$slots: { default: true } 37947 }); 37948 } 37949 }; 37950 37951 var alternate = ($$anchor) => { 37952 { 37953 let $0 = user_derived(() => get(has_value) ? "solid" : "dashed"); 37954 let $1 = user_derived(() => is_dragging ? "focus" : "base"); 37955 37956 Block($$anchor, { 37957 get visible() { 37958 return gradio.shared.visible; 37959 }, 37960 37961 get variant() { 37962 return get($0); 37963 }, 37964 37965 get border_mode() { 37966 return get($1); 37967 }, 37968 padding: false, 37969 get elem_id() { 37970 return gradio.shared.elem_id; 37971 }, 37972 37973 get elem_classes() { 37974 return gradio.shared.elem_classes; 37975 }, 37976 37977 get height() { 37978 return gradio.props.height; 37979 }, 37980 37981 get width() { 37982 return gradio.props.width; 37983 }, 37984 allow_overflow: true, 37985 overflow_behavior: 'visible', 37986 get container() { 37987 return gradio.shared.container; 37988 }, 37989 37990 get scale() { 37991 return gradio.shared.scale; 37992 }, 37993 37994 get min_width() { 37995 return gradio.shared.min_width; 37996 }, 37997 37998 children: ($$anchor, $$slotProps) => { 37999 var fragment_4 = root_4(); 38000 var node_3 = first_child(fragment_4); 38001 38002 Static(node_3, spread_props( 38003 { 38004 get autoscroll() { 38005 return gradio.shared.autoscroll; 38006 }, 38007 38008 get i18n() { 38009 return gradio.i18n; 38010 } 38011 }, 38012 () => gradio.shared.loading_status, 38013 { 38014 on_clear_status: () => gradio.dispatch("clear_status", gradio.shared.loading_status) 38015 } 38016 )); 38017 38018 var node_4 = sibling(node_3, 2); 38019 38020 { 38021 let $0 = user_derived(() => gradio.shared.attached_events.includes("apply")); 38022 let $1 = user_derived(() => gradio.shared.attached_events.includes("change") || gradio.shared.attached_events.includes("input")); 38023 let $2 = user_derived(() => gradio.props.buttons === null ? true : gradio.props.buttons.includes("download")); 38024 38025 bind_this( 38026 InteractiveImageEditor(node_4, { 38027 get border_region() { 38028 return gradio.props.border_region; 38029 }, 38030 38031 get canvas_size() { 38032 return gradio.props.canvas_size; 38033 }, 38034 38035 get layers() { 38036 return get(normalised_layers); 38037 }, 38038 38039 get composite() { 38040 return get(normalised_composite); 38041 }, 38042 38043 get background() { 38044 return get(normalised_background); 38045 }, 38046 38047 get root() { 38048 return gradio.shared.root; 38049 }, 38050 38051 get sources() { 38052 return gradio.props.sources; 38053 }, 38054 38055 get label() { 38056 return gradio.shared.label; 38057 }, 38058 38059 get show_label() { 38060 return gradio.shared.show_label; 38061 }, 38062 38063 get fixed_canvas() { 38064 return gradio.props.fixed_canvas; 38065 }, 38066 38067 get brush() { 38068 return gradio.props.brush; 38069 }, 38070 38071 get eraser() { 38072 return gradio.props.eraser; 38073 }, 38074 38075 get changeable() { 38076 return get($0); 38077 }, 38078 38079 get realtime() { 38080 return get($1); 38081 }, 38082 38083 get i18n() { 38084 return gradio.i18n; 38085 }, 38086 38087 get transforms() { 38088 return gradio.props.transforms; 38089 }, 38090 38091 get accept_blobs() { 38092 return gradio.shared.server.accept_blobs; 38093 }, 38094 38095 get layer_options() { 38096 return gradio.props.layers; 38097 }, 38098 upload: (...args) => gradio.shared.client.upload(...args), 38099 get placeholder() { 38100 return gradio.props.placeholder; 38101 }, 38102 38103 get webcam_options() { 38104 return gradio.props.webcam_options; 38105 }, 38106 38107 get show_download_button() { 38108 return get($2); 38109 }, 38110 38111 get theme_mode() { 38112 return gradio.shared.theme_mode; 38113 }, 38114 38115 get is_dragging() { 38116 return is_dragging; 38117 }, 38118 38119 set is_dragging($$value) { 38120 is_dragging = $$value; 38121 }, 38122 38123 get image_id() { 38124 return get(image_id); 38125 }, 38126 38127 set image_id($$value) { 38128 set(image_id, $$value, true); 38129 }, 38130 38131 $$events: { 38132 input: () => { 38133 if (!has_run_input) { 38134 has_run_input = true; 38135 } else { 38136 gradio.dispatch("input"); 38137 } 38138 }, 38139 save: (e) => handle_save(), 38140 edit: () => gradio.dispatch("edit"), 38141 clear: () => gradio.dispatch("clear"), 38142 drag: ({ detail }) => is_dragging = detail, 38143 upload: () => gradio.dispatch("upload"), 38144 share: ({ detail }) => gradio.dispatch("share", detail), 38145 error: [ 38146 ({ detail }) => { 38147 gradio.shared.loading_status = gradio.shared.loading_status || {}; 38148 gradio.shared.loading_status.status = "error"; 38149 gradio.dispatch("error", detail); 38150 }, 38151 38152 function ($$arg) { 38153 bubble_event.call(this, $$props, $$arg); 38154 } 38155 ], 38156 receive_null: () => gradio.props.value = { background: null, layers: [], composite: null }, 38157 change: () => { 38158 if (!has_run_change) { 38159 has_run_change = true; 38160 } else { 38161 gradio.dispatch("change"); 38162 } 38163 }, 38164 download_error: (e) => gradio.dispatch("error", e.detail) 38165 } 38166 }), 38167 ($$value) => editor_instance = $$value, 38168 () => editor_instance 38169 ); 38170 } 38171 38172 append($$anchor, fragment_4); 38173 }, 38174 $$slots: { default: true } 38175 }); 38176 } 38177 }; 38178 38179 if_block(node, ($$render) => { 38180 if (!gradio.shared.interactive) $$render(consequent); else $$render(alternate, false); 38181 }); 38182 } 38183 38184 append($$anchor, fragment); 38185 pop(); 38186} 38187 38188const Index$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ 38189 __proto__: null, 38190 default: Index 38191}, Symbol.toStringTag, { value: 'Module' })); 38192 38193export { UPDATE_BLEND as $, AbstractRenderer as A, BufferUsage as B, Container as C, DOMAdapter as D, ExtensionType as E, Filter as F, GCManagedHash as G, BigPool as H, getGlobalBounds as I, Bounds as J, TexturePool as K, FilterEffect as L, Matrix as M, Sprite as N, getAttributeInfoFromFormat as O, Point as P, unsafeEvalSupported as Q, RendererType as R, State as S, Ticker as T, UPDATE_PRIORITY as U, GlobalResourceRegistry as V, Rectangle as W, SystemRunner as X, multiplyColors as Y, UPDATE_VISIBLE as Z, UPDATE_COLOR as _, EventEmitter as a, TextureStyle as a0, Color as a1, RenderGroup as a2, getL
38193ocalBounds as a3, RenderTexture as a4, VERSION as a5, deprecation as a6, v8_0_0 as a7, RendererInitHook as a8, Geometry as a9, checkMaxIfStatementsInShader as aa, compileHighShaderGlProgram as ab, colorBitGl as ac, roundPixelsBitGl as ad, generateTextureBatchBitGl as ae, getBatchSamplersUniformGroup as af, nextPow2 as ag, CanvasTextMetrics as ah, fontStringFromTextStyle as ai, getCanvasFillStyle as aj, BatchableGraphics as ak, getAdjustedBlendModeBlend as al, ViewableBuffer as am, BitmapFontManager as an, getBitmapTextLayout as ao, Cache as ap, Graphics as aq, updateQuadBounds as ar, TextStyle as as, GraphicsContextSystem as at, Index$1 as au, STENCIL_MODES as b, Buffer as c, BindGroup as d, extensions as e, fastCopy as f, getTextureBatchBindGroup as g, createIdFromString as h, CLEAR as i, CanvasSource as j, TextureSource as k, UniformGroup as l, CanvasPool as m, compileHighShaderGpuProgram as n, colorBit as o, roundPixelsBit as p, generateTextureBatchBit as q, removeItems as r, Shader as s, Texture as t, uid as u, TextureMatrix as v, warn as w, GpuProgram as x, GlProgram as y, DefaultBatcher as z }; 38194//# sourceMappingURL=Index-DC3EL6G-.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.