/ alienkitty_canvas.html
alienkitty_canvas.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> 6 7 <title>Alien Kitty — Space.js</title> 8 9 <link rel="preconnect" href="https://fonts.gstatic.com"> 10 <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono"> 11 <link rel="stylesheet" href="assets/css/style.css"> 12 13 <script type="module"> 14 import { AssetLoader, Interface, clearTween, degToRad, delayedCall, headsTails, randInt, ticker, tween } from './src/index.js'; 15 16 const assetLoader = new AssetLoader(); 17 const loadImage = path => assetLoader.loadImage(path); 18 19 class AlienKittyCanvas extends Interface { 20 constructor() { 21 super(null, 'canvas'); 22 23 this.width = 90; 24 this.height = 86; 25 this.needsUpdate = false; 26 this.isLoaded = false; 27 28 this.init(); 29 this.initCanvas(); 30 } 31 32 init() { 33 this.css({ 34 opacity: 0 35 }); 36 } 37 38 initCanvas() { 39 this.context = this.element.getContext('2d'); 40 } 41 42 async initImages() { 43 const [alienkitty, eyelid] = await Promise.all([ 44 loadImage('assets/images/alienkitty.svg'), 45 loadImage('assets/images/alienkitty_eyelid.svg') 46 ]); 47 48 this.alienkitty = this.createCanvasObject(alienkitty, 90, 86); 49 this.eyelid1 = this.createCanvasObject(eyelid, 24, 14, { pX: 0.5, x: 35, y: 25, scaleX: 1.5, scaleY: 0.01 }); 50 this.eyelid2 = this.createCanvasObject(eyelid, 24, 14, { x: 53, y: 26, scaleX: 1, scaleY: 0.01 }); 51 52 this.isLoaded = true; 53 54 this.update(); 55 } 56 57 createCanvasObject(image, width, height, { 58 x = 0, 59 y = 0, 60 pX = 0, 61 pY = 0, 62 rotation = 0, 63 scaleX = 1, 64 scaleY = 1, 65 scale = 1, 66 opacity = 1 67 } = {}) { 68 return { 69 image, 70 width, 71 height, 72 x, 73 y, 74 pX: width * pX, 75 pY: height * pY, 76 rotation, 77 scaleX: scaleX * scale, 78 scaleY: scaleY * scale, 79 opacity 80 }; 81 } 82 83 drawImage(object) { 84 const context = this.context; 85 86 context.save(); 87 context.translate(object.x + object.pX, object.y + object.pY); 88 context.rotate(degToRad(object.rotation)); 89 context.scale(object.scaleX, object.scaleY); 90 context.globalAlpha = object.opacity; 91 context.drawImage(object.image, -object.pX, -object.pY, object.width, object.height); 92 context.restore(); 93 } 94 95 addListeners() { 96 ticker.add(this.onUpdate); 97 } 98 99 removeListeners() { 100 ticker.remove(this.onUpdate); 101 } 102 103 blink() { 104 this.timeout = delayedCall(randInt(0, 10000), headsTails(this.onBlink1, this.onBlink2)); 105 } 106 107 // Event handlers 108 109 onUpdate = () => { 110 if (this.needsUpdate) { 111 this.update(); 112 } 113 }; 114 115 onBlink1 = () => { 116 this.needsUpdate = true; 117 tween(this.eyelid1, { scaleY: 1.5 }, 120, 'easeOutCubic', () => { 118 tween(this.eyelid1, { scaleY: 0.01 }, 180, 'easeOutCubic'); 119 }); 120 tween(this.eyelid2, { scaleX: 1.3, scaleY: 1.3 }, 120, 'easeOutCubic', () => { 121 tween(this.eyelid2, { scaleX: 1, scaleY: 0.01 }, 180, 'easeOutCubic', () => { 122 this.needsUpdate = false; 123 this.blink(); 124 }); 125 }); 126 }; 127 128 onBlink2 = () => { 129 this.needsUpdate = true; 130 tween(this.eyelid1, { scaleY: 1.5 }, 120, 'easeOutCubic', () => { 131 tween(this.eyelid1, { scaleY: 0.01 }, 180, 'easeOutCubic'); 132 }); 133 tween(this.eyelid2, { scaleX: 1.3, scaleY: 1.3 }, 180, 'easeOutCubic', () => { 134 tween(this.eyelid2, { scaleX: 1, scaleY: 0.01 }, 240, 'easeOutCubic', () => { 135 this.needsUpdate = false; 136 this.blink(); 137 }); 138 }); 139 }; 140 141 // Public methods 142 143 resize = () => { 144 const dpr = window.devicePixelRatio; 145 146 this.element.width = Math.round(this.width * dpr); 147 this.element.height = Math.round(this.height * dpr); 148 this.element.style.width = `${this.width}px`; 149 this.element.style.height = `${this.height}px`; 150 this.context.scale(dpr, dpr); 151 152 if (this.isLoaded) { 153 this.update(); 154 } 155 }; 156 157 update = () => { 158 this.context.clearRect(0, 0, this.element.width, this.element.height); 159 160 this.drawImage(this.alienkitty); 161 this.drawImage(this.eyelid1); 162 this.drawImage(this.eyelid2); 163 }; 164 165 animateIn = () => { 166 this.addListeners(); 167 this.resize(); 168 this.blink(); 169 170 this.tween({ opacity: 1 }, 1000, 'easeOutSine'); 171 }; 172 173 animateOut = callback => { 174 this.tween({ opacity: 0 }, 500, 'easeInOutQuad', callback); 175 }; 176 177 ready = () => this.initImages(); 178 179 destroy = () => { 180 this.removeListeners(); 181 182 clearTween(this.timeout); 183 clearTween(this.eyelid1); 184 clearTween(this.eyelid2); 185 186 return super.destroy(); 187 }; 188 } 189 190 class App { 191 static async init() { 192 this.initView(); 193 194 this.addListeners(); 195 196 await this.view.ready(); 197 this.view.animateIn(); 198 } 199 200 static initView() { 201 this.view = new AlienKittyCanvas(); 202 this.view.css({ 203 position: 'absolute', 204 left: '50%', 205 top: '50%', 206 marginLeft: -this.view.width / 2, 207 marginTop: -this.view.height / 2 - 65, 208 cursor: 'pointer' 209 }); 210 document.body.appendChild(this.view.element); 211 } 212 213 static addListeners() { 214 this.view.element.addEventListener('click', this.onClick); 215 ticker.start(); 216 } 217 218 // Event handlers 219 220 static onClick = () => { 221 this.view.element.removeEventListener('click', this.onClick); 222 223 this.view.animateOut(() => { 224 this.view = this.view.destroy(); 225 }); 226 }; 227 } 228 229 App.init(); 230 </script> 231 </head> 232 <body> 233 </body> 234 </html>