diff --git a/src/components/App.jsx b/src/components/App.jsx index 04d0c18..edaf23d 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -20,7 +20,6 @@ import Menu from './Menu'; import UI from './UI'; import ExpandMenuButton from './ExpandMenuButton'; import ModalRoot from './ModalRoot'; -import ChristmasButton from './ChristmasButton'; const App = () => (
@@ -36,7 +35,6 @@ const App = () => ( -
); diff --git a/src/components/ChristmasButton.jsx b/src/components/ChristmasButton.jsx deleted file mode 100644 index d82fd7b..0000000 --- a/src/components/ChristmasButton.jsx +++ /dev/null @@ -1,55 +0,0 @@ -/** - * - * @flow - */ - -import React, { useState } from 'react'; -import { GiPineTree } from 'react-icons/gi'; - -import SnowStorm from '../ui/snow'; - -const videoIds = [ - '-YUH8Xfz-jg', - 'yXQViqx6GMY', - 'is4NQkUN3AI', - 'xjLTDaCUYuQ', -]; - -const snowStorm = new SnowStorm(window, document); - -const ChristmasButton = () => { - const [playing, setPlaying] = useState(false); - const prot = window.location.protocol; - - const video = videoIds[Math.floor(Math.random() * videoIds.length)]; - const url = `${prot}//www.youtube.com/embed/${video}?autoplay=1&loop=1`; - - return ( -
{ - setPlaying(!playing); - snowStorm.toggleSnow(); - }} - style={{ - boxShadow: (playing) - ? '0 0 9px 6px rgba(0, 189, 47, 0.8)' - : '0 0 9px 6px rgba(189, 0, 0, 0.8)', - }} - > - - {(playing) && ( - - )} -
- ); -}; - -export default ChristmasButton; diff --git a/src/core/PixelCache.js b/src/core/PixelCache.js new file mode 100644 index 0000000..eff9556 --- /dev/null +++ b/src/core/PixelCache.js @@ -0,0 +1,68 @@ +/* + * Caching pixels for a few ms before sending them + * in bursts per chunk + * @flow + */ + +import webSockets from '../socket/websockets'; + +class PixelCache { + PXL_CACHE: Map; + + constructor() { + this.PXL_CACHE = new Map(); + this.flushCache = this.flushCache.bind(this); + } + + /* + * append pixel to cache + * @param canvasId canvas id + * @param i Chunk coordinates + * @param j Chunk coordinates + * @param offset Offset of pixel within chunk + * @param color color index of pixel + */ + append( + canvasId: number, + color: number, + i: number, + j: number, + offset: number, + ) { + const { PXL_CACHE } = this; + const chunkCanvasId = (canvasId << 16) | (i << 8) | j; + + const pxls = PXL_CACHE.get(chunkCanvasId); + const newpxl = Buffer.allocUnsafe(4); + + newpxl.writeUInt8(offset >>> 16, 0); + newpxl.writeUInt16BE(offset & 0x00FFFF, 1); + newpxl.writeUInt8(color, 3); + + if (typeof pxls === 'undefined') { + PXL_CACHE.set(chunkCanvasId, newpxl); + } else { + PXL_CACHE.set( + chunkCanvasId, + Buffer.concat([pxls, newpxl]), + ); + } + } + + flushCache() { + const { PXL_CACHE: cache } = this; + this.PXL_CACHE = new Map(); + + cache.forEach((pxls, chunkCanvasId) => { + const canvasId = chunkCanvasId & 0xFF0000 >> 16; + const chunkId = chunkCanvasId & 0x00FFFF; + webSockets.broadcastPixels(canvasId, chunkId, pxls); + }); + } +} + +const pixelCache = new PixelCache(); +// send pixels from cache to websockets every 20ms +setInterval(pixelCache.flushCache, 20); + +export default pixelCache; diff --git a/src/core/Player.js b/src/core/Player.js deleted file mode 100644 index 59f0877..0000000 --- a/src/core/Player.js +++ /dev/null @@ -1,16 +0,0 @@ -/* @flow */ - - -class Player { - wait: ?number; // date - - constructor() { - this.wait = null; - } - - setWait(wait) { - this.wait = wait; - } -} - -export default Player; diff --git a/src/core/Void.js b/src/core/Void.js index 8b9e230..b33a763 100644 --- a/src/core/Void.js +++ b/src/core/Void.js @@ -178,23 +178,25 @@ class Void extends WebSocketEvents { const { i: pi, j: pj, - offset: off, - color, + pixels, } = PixelUpdate.hydrate(buffer); - if (color <= 2 || color === 25) { - const { i, j } = this; - // 3x3 chunk area (this is hardcoded on multiple places) - if (pi >= i - 1 && pi <= i + 1 && pj >= j - 1 && pj <= j + 1) { - const uOff = (pi - i + 1) * TILE_SIZE; - const vOff = (pj - j + 1) * TILE_SIZE; - const x = uOff + off % TILE_SIZE; - const y = vOff + Math.floor(off / TILE_SIZE); - if (this.isSet(x, y, true)) { - this.pixelStack.push([x, y]); - } else { - this.userArea[x + y * TILE_SIZE * 3] = color; + const { i, j } = this; + // 3x3 chunk area (this is hardcoded on multiple places) + if (pi >= i - 1 && pi <= i + 1 && pj >= j - 1 && pj <= j + 1) { + pixels.forEach((pxl) => { + const [off, color] = pxl; + if (color <= 2 || color === 25) { + const uOff = (pi - i + 1) * TILE_SIZE; + const vOff = (pj - j + 1) * TILE_SIZE; + const x = uOff + off % TILE_SIZE; + const y = vOff + Math.floor(off / TILE_SIZE); + if (this.isSet(x, y, true)) { + this.pixelStack.push([x, y]); + } else { + this.userArea[x + y * TILE_SIZE * 3] = color; + } } - } + }); } } } diff --git a/src/core/draw.js b/src/core/draw.js index 17b0412..8e51283 100644 --- a/src/core/draw.js +++ b/src/core/draw.js @@ -36,7 +36,6 @@ import { THREE_CANVAS_HEIGHT, THREE_TILE_SIZE, TILE_SIZE } from './constants'; export async function drawByOffsets( user: User, canvasId: number, - color: ColorIndex, i: number, j: number, pixels: Array, @@ -44,6 +43,7 @@ export async function drawByOffsets( let wait = 0; let coolDown = 0; let retCode = 0; + let pxlCnt = 0; const canvas = canvases[canvasId]; if (!canvas) { @@ -54,9 +54,11 @@ export async function drawByOffsets( retCode: 1, }; } - const { size: canvasSize, v: is3d } = canvas; + const { size: canvasSize, v: is3d } = canvas; try { + wait = await user.getWait(canvasId); + const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE; /* * canvas/chunk validation @@ -82,72 +84,82 @@ export async function drawByOffsets( } const isAdmin = (user.userlvl === 1); - /* - * TODO benchmark if requesting by pixel or chunk - * is better - */ - const chunk = await RedisCanvas.getChunk(canvasId, i, j); - const setColor = await RedisCanvas.getPixelByOffset(canvasId, i, j, offset); - - /* - * pixel validation - */ - const maxSize = (is3d) ? tileSize * tileSize * THREE_CANVAS_HEIGHT - : tileSize * tileSize; - if (offset >= maxSize) { - // z out of bounds or weird stuff - throw new Error(4); - } - if (color >= canvas.colors.length - || (color < canvas.cli && !(canvas.v && color === 0)) - ) { - // color out of bounds - throw new Error(5); - } - - if (setColor & 0x80 - /* 3D Canvas Minecraft Avatars */ - // && x >= 96 && x <= 128 && z >= 35 && z <= 100 - // 96 - 128 on x - // 32 - 128 on z - || (canvas.v && i === 19 && j >= 17 && j < 20 && !isAdmin) - ) { - // protected pixel - throw new Error(8); - } - - coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd; - if (isAdmin) { - coolDown = 0.0; - } else if (rpgEvent.success) { + let coolDownFactor = 1; + if (rpgEvent.success) { if (rpgEvent.success === 1) { - // if HOURLY_EVENT got won - coolDown /= 2; + // if hourly event got won + coolDownFactor = 0.5; } else { - // if HOURLY_EVENT got lost - coolDown *= 2; + // if hourly event got lost + coolDownFactor = 2; } } - const now = Date.now(); - wait = await user.getWait(canvasId); - if (!wait) wait = now; - wait += coolDown; - const waitLeft = wait - now; - if (waitLeft > canvas.cds) { - // cooldown stack used - wait = waitLeft - coolDown; - coolDown = canvas.cds - waitLeft; - throw new Error(9); - } + /* + * TODO benchmark if requesting by pixel or chunk is better + */ - setPixelByOffset(canvasId, color, i, j, offset); + while (pixels.length) { + const [offset, color] = pixels.pop(); - user.setWait(waitLeft, canvasId); - if (canvas.ranked) { - user.incrementPixelcount(); + + const [x, y, z] = getPixelFromChunkOffset(i, j, offset, canvasSize, is3d); + pixelLogger.info( + `${user.ip} ${user.id} ${canvasId} ${x} ${y} ${z} ${color} ${retCode}`, + ); + + // eslint-disable-next-line no-await-in-loop + const setColor = await RedisCanvas.getPixelByOffset( + canvasId, + i, j, + offset, + ); + + /* + * pixel validation + */ + const maxSize = (is3d) ? tileSize * tileSize * THREE_CANVAS_HEIGHT + : tileSize * tileSize; + if (offset >= maxSize) { + // z out of bounds or weird stuff + throw new Error(4); + } + if (color >= canvas.colors.length + || (color < canvas.cli && !(canvas.v && color === 0)) + ) { + // color out of bounds + throw new Error(5); + } + + if (setColor & 0x80 + /* 3D Canvas Minecraft Avatars */ + // && x >= 96 && x <= 128 && z >= 35 && z <= 100 + // 96 - 128 on x + // 32 - 128 on z + || (canvas.v && i === 19 && j >= 17 && j < 20 && !isAdmin) + ) { + // protected pixel + throw new Error(8); + } + + coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd; + if (isAdmin) { + coolDown = 0.0; + } else { + coolDown *= coolDownFactor; + } + + wait += coolDown; + if (wait > canvas.cds) { + // cooldown stack used + coolDown = canvas.cds - wait; + wait -= coolDown; + throw new Error(9); + } + + setPixelByOffset(canvasId, color, i, j, offset); + pxlCnt += 1; } - wait = waitLeft; } catch (e) { retCode = parseInt(e.message, 10); if (Number.isNaN(retCode)) { @@ -155,9 +167,12 @@ export async function drawByOffsets( } } - const [x, y, z] = getPixelFromChunkOffset(i, j, offset, canvasSize, is3d); - // eslint-disable-next-line max-len - pixelLogger.info(`${user.ip} ${user.id} ${canvasId} ${x} ${y} ${z} ${color} ${retCode}`); + if (pxlCnt) { + user.setWait(wait, canvasId); + if (canvas.ranked) { + user.incrementPixelcount(pxlCnt); + } + } return { wait, @@ -387,7 +402,8 @@ export function drawSafeByCoords( export function drawSafeByOffsets( user: User, canvasId: number, - color: ColorIndex, + i: number, + j: number, pixels: Array, ): Promise { // can just check for one unique occurence, diff --git a/src/core/setPixel.js b/src/core/setPixel.js index f2b4e22..2539e83 100644 --- a/src/core/setPixel.js +++ b/src/core/setPixel.js @@ -1,14 +1,37 @@ -/* @flow */ +/* + * Set pixels on canvas. + * Pixels get collected in a cache for 5ms and sent to players at once. + * @flow + * */ import RedisCanvas from '../data/models/RedisCanvas'; -import webSockets from '../socket/websockets'; import { getChunkOfPixel, getOffsetOfPixel, } from './utils'; +import pixelCache from './PixelCache'; // eslint-disable-next-line import/no-unresolved import canvases from './canvases.json'; +/** + * + * By Offset is prefered on server side + * @param canvasId + * @param i Chunk coordinates + * @param j + * @param offset Offset of pixel withing chunk + */ +export function setPixelByOffset( + canvasId: number, + color: ColorIndex, + i: number, + j: number, + offset: number, +) { + RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId); + pixelCache.append(canvasId, color, i, j, offset); +} + /** * * @param canvasId @@ -28,25 +51,5 @@ export function setPixelByCoords( const canvasSize = canvases[canvasId].size; const [i, j] = getChunkOfPixel(canvasSize, x, y, z); const offset = getOffsetOfPixel(canvasSize, x, y, z); - RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId); - webSockets.broadcastPixel(canvasId, i, j, offset, color); -} - -/** - * - * By Offset is prefered on server side - * @param canvasId - * @param i Chunk coordinates - * @param j - * @param offset Offset of pixel withing chunk - */ -export function setPixelByOffset( - canvasId: number, - color: ColorIndex, - i: number, - j: number, - offset: number, -) { - RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId); - webSockets.broadcastPixel(canvasId, i, j, offset, color); + setPixelByOffset(canvasId, color, i, j, offset); } diff --git a/src/data/models/User.js b/src/data/models/User.js index 31412d9..553e454 100644 --- a/src/data/models/User.js +++ b/src/data/models/User.js @@ -12,7 +12,6 @@ import redis from '../redis'; import logger from '../../core/logger'; import Model from '../sequelize'; -import RegUser from './RegUser'; import { getIPv6Subnet } from '../../utils/ip'; import { ADMIN_IDS } from '../../core/config'; @@ -39,7 +38,6 @@ class User { this.blocked = []; this.userlvl = 0; this.ipSub = getIPv6Subnet(ip); - this.wait = null; // following gets populated by passport this.regUser = null; } @@ -130,13 +128,12 @@ class User { return (this.regUser) ? this.regUser.name : null; } - async setWait(coolDown: number, canvasId: number): Promise { - if (!coolDown) return false; - this.wait = Date.now() + coolDown; + async setWait(wait: number, canvasId: number): Promise { + if (!wait) return false; // PX is milliseconds expire - await redis.setAsync(`cd:${canvasId}:ip:${this.ipSub}`, '', 'PX', coolDown); + await redis.setAsync(`cd:${canvasId}:ip:${this.ipSub}`, '', 'PX', wait); if (this.id != null) { - await redis.setAsync(`cd:${canvasId}:id:${this.id}`, '', 'PX', coolDown); + await redis.setAsync(`cd:${canvasId}:id:${this.id}`, '', 'PX', wait); } return true; } @@ -151,23 +148,19 @@ class User { } logger.debug('ererer', ttl, typeof ttl); - - const wait = ttl < 0 ? null : Date.now() + ttl; - this.wait = wait; + const wait = ttl < 0 ? 0 : ttl; return wait; } - async incrementPixelcount(): Promise { + async incrementPixelcount(amount: number = 1): Promise { const { id } = this; if (!id) return false; if (this.userlvl === 1) return false; try { - await RegUser.update({ - totalPixels: Sequelize.literal('totalPixels + 1'), - dailyTotalPixels: Sequelize.literal('dailyTotalPixels + 1'), - }, { - where: { id }, - }); + await this.regUser.increment( + ['totalPixels', 'dailyTotalPixels'], + { by: amount }, + ); } catch (err) { return false; } diff --git a/src/socket/APISocketServer.js b/src/socket/APISocketServer.js index 19528c7..d507763 100644 --- a/src/socket/APISocketServer.js +++ b/src/socket/APISocketServer.js @@ -220,7 +220,7 @@ class APISocketServer extends WebSocketEvents { // get userinfo user.ip = ip; const wait = await user.getWait(0); - const waitSeconds = (wait) ? (wait - Date.now()) / 1000 : null; + const waitSeconds = (wait) ? wait / 1000 : null; const name = (user.id == null) ? null : user.regUser.name; ws.send(JSON.stringify([ 'mcme', diff --git a/src/socket/ProtocolClient.js b/src/socket/ProtocolClient.js index 5009b7b..c3378cb 100644 --- a/src/socket/ProtocolClient.js +++ b/src/socket/ProtocolClient.js @@ -1,4 +1,4 @@ - * @flow +/* @flow */ // allow the websocket to be noisy on the console diff --git a/src/socket/SocketServer.js b/src/socket/SocketServer.js index ba8066c..9d073bb 100644 --- a/src/socket/SocketServer.js +++ b/src/socket/SocketServer.js @@ -22,7 +22,7 @@ import chatProvider, { ChatProvider } from '../core/ChatProvider'; import authenticateClient from './verifyClient'; import WebSocketEvents from './WebSocketEvents'; import webSockets from './websockets'; -import { drawSafeByOffset } from '../core/draw'; +import { drawSafeByOffsets } from '../core/draw'; import { needCaptcha } from '../utils/captcha'; import { cheapDetector } from '../core/isProxy'; @@ -385,18 +385,17 @@ class SocketServer extends WebSocketEvents { } // receive pixels here const { - i, j, offset, - color, + i, j, pixels, } = PixelUpdate.hydrate(buffer); const { wait, coolDown, retCode, - } = await drawSafeByOffset( + } = await drawSafeByOffsets( ws.user, ws.canvasId, - color, - i, j, offset, + i, j, + pixels, ); ws.send(PixelReturn.dehydrate(retCode, wait, coolDown)); break; @@ -408,8 +407,7 @@ class SocketServer extends WebSocketEvents { } ws.canvasId = canvasId; const wait = await ws.user.getWait(canvasId); - const waitMs = (wait) ? wait - Date.now() : 0; - ws.send(CoolDownPacket.dehydrate(waitMs)); + ws.send(CoolDownPacket.dehydrate(wait)); break; } case RegisterChunk.OP_CODE: { diff --git a/src/socket/packets/PixelUpdateClient.js b/src/socket/packets/PixelUpdateClient.js index 731f798..2890c66 100644 --- a/src/socket/packets/PixelUpdateClient.js +++ b/src/socket/packets/PixelUpdateClient.js @@ -2,12 +2,10 @@ * Packet for sending and receiving pixels per chunk * Multiple pixels can be sent at once * - * @flow + * @flow */ -import type { ColorIndex } from '../../core/Palette'; - type PixelUpdatePacket = { x: number, y: number, @@ -35,7 +33,7 @@ export default { const color = data.getUint8(off -= 1); const offsetL = data.getUint16(off -= 2); const offsetH = data.getUint8(off -= 1) << 16; - const pixels.push([offsetH | offsetL, color]); + pixels.push([offsetH | offsetL, color]); } return { i, j, pixels, @@ -57,8 +55,8 @@ export default { * 1 byte color */ let cnt = 2; - for (let i = 0; i < pixels.length; i += 1) { - const [offset, color] = pixels[i]; + for (let p = 0; p < pixels.length; p += 1) { + const [offset, color] = pixels[p]; view.setUint8(cnt += 1, offset >>> 16); view.setUint16(cnt += 1, offset & 0x00FFFF); view.setUint8(cnt += 2, color); diff --git a/src/socket/packets/PixelUpdateServer.js b/src/socket/packets/PixelUpdateServer.js index 70a7740..17344a1 100644 --- a/src/socket/packets/PixelUpdateServer.js +++ b/src/socket/packets/PixelUpdateServer.js @@ -1,8 +1,6 @@ /* @flow */ -import type { ColorIndex } from '../../core/Palette'; - type PixelUpdatePacket = { x: number, y: number, @@ -26,38 +24,29 @@ export default { */ const pixels = []; let off = data.length; - while (off >= 3) { + /* + * limit the max amount of pixels that can be + * receive to 500 + */ + let pxlcnt = 0; + while (off >= 3 && pxlcnt < 500) { const color = data.readUInt8(off -= 1); const offsetL = data.readUInt16BE(off -= 2); const offsetH = data.readUInt8(off -= 1) << 16; - const pixels.push([offsetH | offsetL, color]); + pixels.push([offsetH | offsetL, color]); + pxlcnt += 1; } return { i, j, pixels, }; }, - dehydrate(i, j, pixels): Buffer { - const buffer = Buffer.allocUnsafe(1 + 1 + 1 + pixels.length * 4); - buffer.writeUInt8(OP_CODE, 0); - /* - * chunk coordinates - */ - buffer.writeUInt8(i, 1); - buffer.writeUInt8(j, 2); - /* - * offset and color of every pixel - * 3 bytes offset - * 1 byte color - */ - let cnt = 2; - for (let i = 0; i < pixels.length; i += 1) { - const [offset, color] = pixels[i]; - buffer.writeUInt8(offset >>> 16, cnt += 1); - buffer.writeUInt16BE(offset & 0x00FFFF, cnt += 1); - buffer.writeUInt8(color, cnt += 2); - } - - return buffer; + /* + * @param chunkId id consisting of chunk coordinates + * @param pixels Buffer with offset and color of one or more pixels + */ + dehydrate(chunkId, pixels): Buffer { + const index = new Uint8Array([OP_CODE, chunkId >> 8, chunkId && 0xFF]); + return Buffer.concat([index, pixels]); }, }; diff --git a/src/socket/websockets.js b/src/socket/websockets.js index ceb29d9..80260a7 100644 --- a/src/socket/websockets.js +++ b/src/socket/websockets.js @@ -41,23 +41,18 @@ class WebSockets { /* * broadcast pixel message via websocket - * @param canvasIdent ident of canvas - * @param i x coordinates of chunk - * @param j y coordinates of chunk - * @param offset offset of pixel within this chunk - * @param color colorindex + * @param canvasId ident of canvas + * @param chunkid id consisting of i,j chunk coordinates + * @param pxls buffer with offset and color of one or more pixels */ - broadcastPixel( + broadcastPixels( canvasId: number, - i: number, - j: number, - offset: number, - color: number, + chunkId: number, + pixels: Buffer, ) { - const chunkid = (i << 8) | j; - const buffer = PixelUpdate.dehydrate(i, j, offset, color); + const buffer = PixelUpdate.dehydrate(chunkId, pixels); this.listeners.forEach( - (listener) => listener.broadcastPixelBuffer(canvasId, chunkid, buffer), + (listener) => listener.broadcastPixelBuffer(canvasId, chunkId, buffer), ); } diff --git a/src/ui/snow.js b/src/ui/snow.js deleted file mode 100644 index 38fd3da..0000000 --- a/src/ui/snow.js +++ /dev/null @@ -1,670 +0,0 @@ -/** @license - * DHTML Snowstorm! JavaScript-based snow for web pages - * Making it snow on the internets since 2003. You're welcome. - * ----------------------------------------------------------- - * Version 1.44.20131215 (Previous rev: 1.44.20131208) - * Copyright (c) 2007, Scott Schiller. All rights reserved. - * Code provided under the BSD License - * http://schillmania.com/projects/snowstorm/license.txt - */ - -/*jslint nomen: true, plusplus: true, sloppy: true, vars: true, white: true */ -/*global window, document, navigator, clearInterval, setInterval */ - -/* eslint-disable */ - -function SnowStorm(window, document) { - - // --- common properties --- - - this.autoStart = false; // Whether the snow should start automatically or not. - this.flakesMax = 256; // Limit total amount of snow made (falling + sticking) - this.flakesMaxActive = 128; // Limit amount of snow falling at once (less = lower CPU use) - this.animationInterval = 33; // Theoretical "miliseconds per frame" measurement. 20 = fast + smooth, but high CPU use. 50 = more conservative, but slower - this.useGPU = true; // Enable transform-based hardware acceleration, reduce CPU load. - this.className = null; // CSS class name for further customization on snow elements - this.excludeMobile = false; // Snow is likely to be bad news for mobile phones' CPUs (and batteries.) By default, be nice. - this.flakeBottom = null; // Integer for Y axis snow limit, 0 or null for "full-screen" snow effect - this.followMouse = true; // Snow movement can respond to the user's mouse - this.snowColor = '#fff'; // Don't eat (or use?) yellow snow. - this.snowCharacter = '•'; // • = bullet, · is square on some systems etc. - this.snowStick = true; // Whether or not snow should "stick" at the bottom. When off, will never collect. - this.targetElement = null; // element which snow will be appended to (null = document.body) - can be an element ID eg. 'myDiv', or a DOM node reference - this.useMeltEffect = true; // When recycling fallen snow (or rarely, when falling), have it "melt" and fade out if browser supports it - this.useTwinkleEffect = false; // Allow snow to randomly "flicker" in and out of view while falling - this.usePositionFixed = false; // true = snow does not shift vertically when scrolling. May increase CPU load, disabled by default - if enabled, used only where supported - this.usePixelPosition = false; // Whether to use pixel values for snow top/left vs. percentages. Auto-enabled if body is position:relative or targetElement is specified. - - // --- less-used bits --- - - this.freezeOnBlur = true; // Only snow when the window is in focus (foreground.) Saves CPU. - this.flakeLeftOffset = 0; // Left margin/gutter space on edge of container (eg. browser window.) Bump up these values if seeing horizontal scrollbars. - this.flakeRightOffset = 0; // Right margin/gutter space on edge of container - this.flakeWidth = 8; // Max pixel width reserved for snow element - this.flakeHeight = 8; // Max pixel height reserved for snow element - this.vMaxX = 5; // Maximum X velocity range for snow - this.vMaxY = 4; // Maximum Y velocity range for snow - this.zIndex = 0; // CSS stacking order applied to each snowflake - - // --- "No user-serviceable parts inside" past this point, yadda yadda --- - - var storm = this, - features, - // UA sniffing and backCompat rendering mode checks for fixed position, etc. - isIE = navigator.userAgent.match(/msie/i), - isIE6 = navigator.userAgent.match(/msie 6/i), - isMobile = navigator.userAgent.match(/mobile|opera m(ob|in)/i), - isBackCompatIE = (isIE && document.compatMode === 'BackCompat'), - noFixed = (isBackCompatIE || isIE6), - screenX = null, screenX2 = null, screenY = null, scrollY = null, docHeight = null, vRndX = null, vRndY = null, - windOffset = 1, - windMultiplier = 2, - flakeTypes = 6, - fixedForEverything = false, - targetElementIsRelative = false, - opacitySupported = (function(){ - try { - document.createElement('div').style.opacity = '0.5'; - } catch(e) { - return false; - } - return true; - }()), - didInit = false, - docFrag = document.createDocumentFragment(); - - features = (function() { - - var getAnimationFrame; - - /** - * hat tip: paul irish - * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ - * https://gist.github.com/838785 - */ - - function timeoutShim(callback) { - window.setTimeout(callback, 1000/(storm.animationInterval || 20)); - } - - var _animationFrame = (window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - timeoutShim); - - // apply to window, avoid "illegal invocation" errors in Chrome - getAnimationFrame = _animationFrame ? function() { - return _animationFrame.apply(window, arguments); - } : null; - - var testDiv; - - testDiv = document.createElement('div'); - - function has(prop) { - - // test for feature support - var result = testDiv.style[prop]; - return (result !== undefined ? prop : null); - - } - - // note local scope. - var localFeatures = { - - transform: { - ie: has('-ms-transform'), - moz: has('MozTransform'), - opera: has('OTransform'), - webkit: has('webkitTransform'), - w3: has('transform'), - prop: null // the normalized property value - }, - - getAnimationFrame: getAnimationFrame - - }; - - localFeatures.transform.prop = ( - localFeatures.transform.w3 || - localFeatures.transform.moz || - localFeatures.transform.webkit || - localFeatures.transform.ie || - localFeatures.transform.opera - ); - - testDiv = null; - - return localFeatures; - - }()); - - this.timer = null; - this.flakes = []; - this.disabled = false; - this.active = false; - this.meltFrameCount = 20; - this.meltFrames = []; - - this.setXY = function(o, x, y) { - - if (!o) { - return false; - } - - if (storm.usePixelPosition || targetElementIsRelative) { - - o.style.left = (x - storm.flakeWidth) + 'px'; - o.style.top = (y - storm.flakeHeight) + 'px'; - - } else if (noFixed) { - - o.style.right = (100-(x/screenX*100)) + '%'; - // avoid creating vertical scrollbars - o.style.top = (Math.min(y, docHeight-storm.flakeHeight)) + 'px'; - - } else { - - if (!storm.flakeBottom) { - - // if not using a fixed bottom coordinate... - o.style.right = (100-(x/screenX*100)) + '%'; - o.style.bottom = (100-(y/screenY*100)) + '%'; - - } else { - - // absolute top. - o.style.right = (100-(x/screenX*100)) + '%'; - o.style.top = (Math.min(y, docHeight-storm.flakeHeight)) + 'px'; - - } - - } - - }; - - this.events = (function() { - - var old = (!window.addEventListener && window.attachEvent), slice = Array.prototype.slice, - evt = { - add: (old?'attachEvent':'addEventListener'), - remove: (old?'detachEvent':'removeEventListener') - }; - - function getArgs(oArgs) { - var args = slice.call(oArgs), len = args.length; - if (old) { - args[1] = 'on' + args[1]; // prefix - if (len > 3) { - args.pop(); // no capture - } - } else if (len === 3) { - args.push(false); - } - return args; - } - - function apply(args, sType) { - var element = args.shift(), - method = [evt[sType]]; - if (old) { - element[method](args[0], args[1]); - } else { - element[method].apply(element, args); - } - } - - function addEvent() { - apply(getArgs(arguments), 'add'); - } - - function removeEvent() { - apply(getArgs(arguments), 'remove'); - } - - return { - add: addEvent, - remove: removeEvent - }; - - }()); - - function rnd(n,min) { - if (isNaN(min)) { - min = 0; - } - return (Math.random()*n)+min; - } - - function plusMinus(n) { - return (parseInt(rnd(2),10)===1?n*-1:n); - } - - this.randomizeWind = function() { - var i; - vRndX = plusMinus(rnd(storm.vMaxX,0.2)); - vRndY = rnd(storm.vMaxY,0.2); - if (this.flakes) { - for (i=0; i=0 && s.vX<0.2) { - s.vX = 0.2; - } else if (s.vX<0 && s.vX>-0.2) { - s.vX = -0.2; - } - if (s.vY>=0 && s.vY<0.2) { - s.vY = 0.2; - } - }; - - this.move = function() { - var vX = s.vX*windOffset, yDiff; - s.x += vX; - s.y += (s.vY*s.vAmp); - if (s.x >= screenX || screenX-s.x < storm.flakeWidth) { // X-axis scroll check - s.x = 0; - } else if (vX < 0 && s.x-storm.flakeLeftOffset < -storm.flakeWidth) { - s.x = screenX-storm.flakeWidth-1; // flakeWidth; - } - s.refresh(); - yDiff = screenY+scrollY-s.y+storm.flakeHeight; - if (yDiff0.998) { - // ~1/1000 chance of melting mid-air, with each frame - s.melting = true; - s.melt(); - // only incrementally melt one frame - // s.melting = false; - } - if (storm.useTwinkleEffect) { - if (s.twinkleFrame < 0) { - if (Math.random() > 0.97) { - s.twinkleFrame = parseInt(Math.random() * 8, 10); - } - } else { - s.twinkleFrame--; - if (!opacitySupported) { - s.o.style.visibility = (s.twinkleFrame && s.twinkleFrame % 2 === 0 ? 'hidden' : 'visible'); - } else { - s.o.style.opacity = (s.twinkleFrame && s.twinkleFrame % 2 === 0 ? 0 : 1); - } - } - } - } - }; - - this.animate = function() { - // main animation loop - // move, check status, die etc. - s.move(); - }; - - this.setVelocities = function() { - s.vX = vRndX+rnd(storm.vMaxX*0.12,0.1); - s.vY = vRndY+rnd(storm.vMaxY*0.12,0.1); - }; - - this.setOpacity = function(o,opacity) { - if (!opacitySupported) { - return false; - } - o.style.opacity = opacity; - }; - - this.melt = function() { - if (!storm.useMeltEffect || !s.melting) { - s.recycle(); - } else { - if (s.meltFrame < s.meltFrameCount) { - s.setOpacity(s.o,s.meltFrames[s.meltFrame]); - s.o.style.fontSize = s.fontSize-(s.fontSize*(s.meltFrame/s.meltFrameCount))+'px'; - s.o.style.lineHeight = storm.flakeHeight+2+(storm.flakeHeight*0.75*(s.meltFrame/s.meltFrameCount))+'px'; - s.meltFrame++; - } else { - s.recycle(); - } - } - }; - - this.recycle = function() { - s.o.style.display = 'none'; - s.o.style.position = (fixedForEverything?'fixed':'absolute'); - s.o.style.bottom = 'auto'; - s.setVelocities(); - s.vCheck(); - s.meltFrame = 0; - s.melting = false; - s.setOpacity(s.o,1); - s.o.style.padding = '0px'; - s.o.style.margin = '0px'; - s.o.style.fontSize = s.fontSize+'px'; - s.o.style.lineHeight = (storm.flakeHeight+2)+'px'; - s.o.style.textAlign = 'center'; - s.o.style.verticalAlign = 'baseline'; - s.x = parseInt(rnd(screenX-storm.flakeWidth-20),10); - s.y = parseInt(rnd(screenY)*-1,10)-storm.flakeHeight; - s.refresh(); - s.o.style.display = 'block'; - s.active = 1; - }; - - this.recycle(); // set up x/y coords etc. - this.refresh(); - - }; - - this.snow = function() { - var active = 0, flake = null, i, j; - for (i=0, j=storm.flakes.length; istorm.flakesMaxActive) { - storm.flakes[storm.flakes.length-1].active = -1; - } - } - storm.targetElement.appendChild(docFrag); - }; - - this.timerInit = function() { - storm.timer = true; - storm.snow(); - }; - - this.init = function() { - var i; - for (i=0; i