diff --git a/README.md b/README.md index 6459ec56..8cee7ecb 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ [![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/tonybaloney.vscode-pets?logo=visualstudio)](https://marketplace.visualstudio.com/items?itemName=tonybaloney.vscode-pets&WT.mc_id=python-17801-anthonyshaw) [![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/tonybaloney.vscode-pets?logo=visualstudio)](https://marketplace.visualstudio.com/items?itemName=tonybaloney.vscode-pets&WT.mc_id=python-17801-anthonyshaw) -![screenshot](https://github.com/tonybaloney/vscode-pets/raw/main/docs/source/_static/screenshot.gif) +![screenshot](https://github.com/tonybaloney/vscode-pets/raw/main/docs/source/_static/winter.gif) ## Installation @@ -60,6 +60,8 @@ Visit the [Crowdin Project](https://crowdin.com/project/vscode-pets) in case you The cat animations were designed by [seethingswarm](https://seethingswarm.itch.io/catset). The dog media assets for this extension were designed by [NVPH Studio](https://nvph-studio.itch.io/dog-animation-4-different-dogs). +The winter theme is original artwork by [Kiana Mosser](https://www.instagram.com/kianamosser/) created for VS Code Pets. + The forest theme was designed by [edermunizz](https://edermunizz.itch.io/free-pixel-art-forest). The castle assets were created using artwork by [GuttyKreum](https://guttykreum.itch.io/gothic-castle-game-assets). [Marc Duiker](https://twitter.com/marcduiker) created the Clippy, Rocky, Zappy, rubber duck, snake, cockatiel, Ferris the crab, and Mod the dotnet bot media assets. diff --git a/docs/source/_static/winter-screenshot.png b/docs/source/_static/winter-screenshot.png new file mode 100644 index 00000000..9962a4f9 Binary files /dev/null and b/docs/source/_static/winter-screenshot.png differ diff --git a/docs/source/_static/winter.gif b/docs/source/_static/winter.gif new file mode 100644 index 00000000..6fb66ac2 Binary files /dev/null and b/docs/source/_static/winter.gif differ diff --git a/docs/source/usage.rst b/docs/source/usage.rst index baa09a87..4a911593 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -142,3 +142,9 @@ Set ``vscode-pets.theme`` to ``"castle"`` for them to roam the ramparts! Set ``vscode-pets.theme`` to ``"beach"`` for your friends to play by the ocean. .. image:: _static/beach-pose.png + +Set ``vscode-pets.theme`` to ``"winter"`` for your pets roam around the snowy mountains. + +.. image:: _static/winter.gif + +If you find the snowfall too distracting, you can disable special effects in settings. \ No newline at end of file diff --git a/media/backgrounds/winter/background-dark-large.png b/media/backgrounds/winter/background-dark-large.png new file mode 100644 index 00000000..30ae3a95 Binary files /dev/null and b/media/backgrounds/winter/background-dark-large.png differ diff --git a/media/backgrounds/winter/background-dark-medium.png b/media/backgrounds/winter/background-dark-medium.png new file mode 100644 index 00000000..b5f5f78e Binary files /dev/null and b/media/backgrounds/winter/background-dark-medium.png differ diff --git a/media/backgrounds/winter/background-dark-nano.png b/media/backgrounds/winter/background-dark-nano.png new file mode 100644 index 00000000..8168a71e Binary files /dev/null and b/media/backgrounds/winter/background-dark-nano.png differ diff --git a/media/backgrounds/winter/background-dark-small.png b/media/backgrounds/winter/background-dark-small.png new file mode 100644 index 00000000..c41491b9 Binary files /dev/null and b/media/backgrounds/winter/background-dark-small.png differ diff --git a/media/backgrounds/winter/background-light-large.png b/media/backgrounds/winter/background-light-large.png new file mode 100644 index 00000000..30ae3a95 Binary files /dev/null and b/media/backgrounds/winter/background-light-large.png differ diff --git a/media/backgrounds/winter/background-light-medium.png b/media/backgrounds/winter/background-light-medium.png new file mode 100644 index 00000000..b5f5f78e Binary files /dev/null and b/media/backgrounds/winter/background-light-medium.png differ diff --git a/media/backgrounds/winter/background-light-nano.png b/media/backgrounds/winter/background-light-nano.png new file mode 100644 index 00000000..8168a71e Binary files /dev/null and b/media/backgrounds/winter/background-light-nano.png differ diff --git a/media/backgrounds/winter/background-light-small.png b/media/backgrounds/winter/background-light-small.png new file mode 100644 index 00000000..c41491b9 Binary files /dev/null and b/media/backgrounds/winter/background-light-small.png differ diff --git a/media/backgrounds/winter/background.png b/media/backgrounds/winter/background.png new file mode 100644 index 00000000..b5f5f78e Binary files /dev/null and b/media/backgrounds/winter/background.png differ diff --git a/media/backgrounds/winter/foreground-dark-large.png b/media/backgrounds/winter/foreground-dark-large.png new file mode 100644 index 00000000..d9938817 Binary files /dev/null and b/media/backgrounds/winter/foreground-dark-large.png differ diff --git a/media/backgrounds/winter/foreground-dark-medium.png b/media/backgrounds/winter/foreground-dark-medium.png new file mode 100644 index 00000000..ae51bb8a Binary files /dev/null and b/media/backgrounds/winter/foreground-dark-medium.png differ diff --git a/media/backgrounds/winter/foreground-dark-nano.png b/media/backgrounds/winter/foreground-dark-nano.png new file mode 100644 index 00000000..2ae806dc Binary files /dev/null and b/media/backgrounds/winter/foreground-dark-nano.png differ diff --git a/media/backgrounds/winter/foreground-dark-small.png b/media/backgrounds/winter/foreground-dark-small.png new file mode 100644 index 00000000..eef7b45f Binary files /dev/null and b/media/backgrounds/winter/foreground-dark-small.png differ diff --git a/media/backgrounds/winter/foreground-light-large.png b/media/backgrounds/winter/foreground-light-large.png new file mode 100644 index 00000000..d9938817 Binary files /dev/null and b/media/backgrounds/winter/foreground-light-large.png differ diff --git a/media/backgrounds/winter/foreground-light-medium.png b/media/backgrounds/winter/foreground-light-medium.png new file mode 100644 index 00000000..ae51bb8a Binary files /dev/null and b/media/backgrounds/winter/foreground-light-medium.png differ diff --git a/media/backgrounds/winter/foreground-light-nano.png b/media/backgrounds/winter/foreground-light-nano.png new file mode 100644 index 00000000..2ae806dc Binary files /dev/null and b/media/backgrounds/winter/foreground-light-nano.png differ diff --git a/media/backgrounds/winter/foreground-light-small.png b/media/backgrounds/winter/foreground-light-small.png new file mode 100644 index 00000000..eef7b45f Binary files /dev/null and b/media/backgrounds/winter/foreground-light-small.png differ diff --git a/media/backgrounds/winter/foreground.png b/media/backgrounds/winter/foreground.png new file mode 100644 index 00000000..ae51bb8a Binary files /dev/null and b/media/backgrounds/winter/foreground.png differ diff --git a/media/pets.css b/media/pets.css index 6bc7d4c9..5c7a5f12 100644 --- a/media/pets.css +++ b/media/pets.css @@ -110,13 +110,26 @@ textarea::placeholder { color: var(--vscode-input-placeholderForeground); } -#petCanvas{ - position:fixed; - bottom:0; - left:0; +#petCanvasContainer { + position: relative; + width: 100%; + height: 100%; +} + +#petCanvasContainer canvas { + position: fixed; + bottom: 0; + left: 0; z-index: 3; } +#ballCanvas { + z-index: 3; +} +#effectCanvas { + z-index: 4; +} + img.pet { -webkit-transform: scaleX(-1); transform: scaleX(-1); diff --git a/package.json b/package.json index 068ff36e..29d404b8 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,8 @@ "none", "forest", "castle", - "beach" + "beach", + "winter" ], "default": "none", "description": "Background theme assets for your pets" @@ -237,6 +238,11 @@ "type": "boolean", "default": false, "description": "Throw ball with mouse" + }, + "vscode-pets.disableEffects": { + "type": "boolean", + "default": false, + "description": "Disable special effects like snowfall" } } } diff --git a/src/common/types.ts b/src/common/types.ts index 9e9f82ca..d751b5de 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -79,6 +79,7 @@ export const enum ColorThemeKind { light = 1, dark = 2, highContrast = 3, + highContrastLight = 4, } export class WebviewMessage { @@ -141,4 +142,10 @@ export const ALL_SCALES = [ PetSize.medium, PetSize.large, ]; -export const ALL_THEMES = [Theme.none, Theme.forest, Theme.castle, Theme.beach]; +export const ALL_THEMES = [ + Theme.none, + Theme.forest, + Theme.castle, + Theme.beach, + Theme.winter, +]; diff --git a/src/extension/extension.ts b/src/extension/extension.ts index ed3c3709..0ae3d4a2 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -85,6 +85,19 @@ function getThrowWithMouseConfiguration(): boolean { .get('throwBallWithMouse', true); } +function getEffectsDisabledConfiguration(): boolean { + return vscode.workspace + .getConfiguration('vscode-pets') + .get('disableEffects', false); +} + +function updatePanelDisableEffects(): void { + const panel = getPetPanel(); + if (panel !== undefined) { + panel.updateDisableEffects(getEffectsDisabledConfiguration()); + } +} + function updatePanelThrowWithMouse(): void { const panel = getPetPanel(); if (panel !== undefined) { @@ -300,6 +313,7 @@ export function activate(context: vscode.ExtensionContext) { getConfiguredTheme(), getConfiguredThemeKind(), getThrowWithMouseConfiguration(), + getEffectsDisabledConfiguration(), ); if (PetPanel.currentPanel) { @@ -346,6 +360,7 @@ export function activate(context: vscode.ExtensionContext) { getConfiguredTheme(), getConfiguredThemeKind(), getThrowWithMouseConfiguration(), + getEffectsDisabledConfiguration(), ); updateExtensionPositionContext().catch((e) => { console.error(e); @@ -636,6 +651,10 @@ export function activate(context: vscode.ExtensionContext) { if (e.affectsConfiguration('vscode-pets.throwBallWithMouse')) { updatePanelThrowWithMouse(); } + + if (e.affectsConfiguration('vscode-pets.disableEffects')) { + updatePanelDisableEffects(); + } }, ), ); @@ -658,6 +677,7 @@ export function activate(context: vscode.ExtensionContext) { getConfiguredTheme(), getConfiguredThemeKind(), getThrowWithMouseConfiguration(), + getEffectsDisabledConfiguration(), ); }, }); @@ -700,6 +720,7 @@ interface IPetPanel { updateTheme(newTheme: Theme, themeKind: vscode.ColorThemeKind): void; update(): void; setThrowWithMouse(newThrowWithMouse: boolean): void; + updateDisableEffects(disableEffects: boolean): void; } class PetWebviewContainer implements IPetPanel { @@ -711,6 +732,7 @@ class PetWebviewContainer implements IPetPanel { protected _theme: Theme; protected _themeKind: vscode.ColorThemeKind; protected _throwBallWithMouse: boolean; + protected _disableEffects: boolean; constructor( extensionUri: vscode.Uri, @@ -720,6 +742,7 @@ class PetWebviewContainer implements IPetPanel { theme: Theme, themeKind: ColorThemeKind, throwBallWithMouse: boolean, + disableEffects: boolean, ) { this._extensionUri = extensionUri; this._petColor = color; @@ -728,6 +751,7 @@ class PetWebviewContainer implements IPetPanel { this._theme = theme; this._themeKind = themeKind; this._throwBallWithMouse = throwBallWithMouse; + this._disableEffects = disableEffects; } public petColor(): PetColor { @@ -754,6 +778,10 @@ class PetWebviewContainer implements IPetPanel { return this._throwBallWithMouse; } + public disableEffects(): boolean { + return this._disableEffects; + } + public updatePetColor(newColor: PetColor) { this._petColor = newColor; } @@ -779,6 +807,14 @@ class PetWebviewContainer implements IPetPanel { }); } + public updateDisableEffects(disableEffects: boolean): void { + this._disableEffects = disableEffects; + void this.getWebview().postMessage({ + command: 'disable-effects', + disabled: disableEffects, + }); + } + public throwBall() { void this.getWebview().postMessage({ command: 'throw-ball', @@ -900,12 +936,14 @@ class PetWebviewContainer implements IPetPanel { VS Code Pets - - +
+ + +
- + `; } @@ -943,6 +981,7 @@ class PetPanel extends PetWebviewContainer implements IPetPanel { theme: Theme, themeKind: ColorThemeKind, throwBallWithMouse: boolean, + disableEffects: boolean, ) { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn @@ -981,6 +1020,7 @@ class PetPanel extends PetWebviewContainer implements IPetPanel { theme, themeKind, throwBallWithMouse, + disableEffects, ); } @@ -1014,6 +1054,7 @@ class PetPanel extends PetWebviewContainer implements IPetPanel { theme: Theme, themeKind: ColorThemeKind, throwBallWithMouse: boolean, + disableEffects: boolean, ) { PetPanel.currentPanel = new PetPanel( panel, @@ -1024,6 +1065,7 @@ class PetPanel extends PetWebviewContainer implements IPetPanel { theme, themeKind, throwBallWithMouse, + disableEffects, ); } @@ -1036,6 +1078,7 @@ class PetPanel extends PetWebviewContainer implements IPetPanel { theme: Theme, themeKind: ColorThemeKind, throwBallWithMouse: boolean, + disableEffects: boolean, ) { super( extensionUri, @@ -1045,6 +1088,7 @@ class PetPanel extends PetWebviewContainer implements IPetPanel { theme, themeKind, throwBallWithMouse, + disableEffects, ); this._panel = panel; @@ -1153,6 +1197,7 @@ async function createPetPlayground(context: vscode.ExtensionContext) { getConfiguredTheme(), getConfiguredThemeKind(), getThrowWithMouseConfiguration(), + getEffectsDisabledConfiguration(), ); if (PetPanel.currentPanel) { var collection = PetSpecification.collectionFromMemento( diff --git a/src/panel/effects/snow.ts b/src/panel/effects/snow.ts index 7ee7a65d..41fce493 100644 --- a/src/panel/effects/snow.ts +++ b/src/panel/effects/snow.ts @@ -16,7 +16,11 @@ class Vector2 { } function floorRandom(min: number, max: number) { - return Math.floor(Math.random() * (max - min + 1) + min); + return (min || 0) + Math.random() * ((max || 1) - (min || 0)); +} + +function microtime(): number { + return new Date().getTime() * 0.001; } class Particle { @@ -64,15 +68,17 @@ export class SnowEffect implements Effect { startTime: number = 0; frameTime: number = 0; - pAmount: number = 5000; // Snowiness + pAmount: number = 2500; // Snowiness pSize: number[] = [0.5, 1.5]; // min and max size - pSwing: number[] = [0.1, 1]; // min and max oscilation speed for x movement - pSpeed: number[] = [40, 100]; // min and max y speed - pAmplitude: number[] = [25, 50]; // min and max distance for x movement + pSwing: number[] = [0.1, 1]; // min and max oscillation speed for x movement + pSpeed: number[] = [10, 50]; // min and max y speed + pAmplitude: number[] = [5, 20]; // min and max distance for x movement + + floor: number = 0; enable(): void { this.running = true; - this.startTime = this.frameTime = Date.now(); + this.startTime = this.frameTime = microtime(); this.loop(); } @@ -84,11 +90,31 @@ export class SnowEffect implements Effect { canvas: HTMLCanvasElement, scale: PetSize, floor: number, + // eslint-disable-next-line no-unused-vars themeKind: ColorThemeKind, ): void { // use the container width and height this.canvas = canvas; this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D; + this.floor = floor; + switch (scale) { + case PetSize.nano: + this.pSize = [0.1, 0.5]; + this.pAmount = 5000; + break; + case PetSize.small: + this.pSize = [0.5, 1.5]; + this.pAmount = 2500; + break; + case PetSize.medium: + this.pSize = [1, 2]; + this.pAmount = 1000; + break; + case PetSize.large: + this.pSize = [1.5, 3]; + this.pAmount = 500; + break; + } this.initParticles(); } @@ -98,11 +124,14 @@ export class SnowEffect implements Effect { this.update(); this.draw(); this.queue(); + } else { + console.log('Snow effect stopped'); } } private initParticles() { if (!this.canvas) { + console.log('Canvas not initialized'); return; } // clear the particles array @@ -128,17 +157,21 @@ export class SnowEffect implements Effect { private update() { if (!this.canvas) { + console.log('Canvas not initialized'); return; } // calculate the time since the last frame - var timeNow = Date.now(); + var timeNow = microtime(); var timeDelta = timeNow - this.frameTime; for (var i = 0; i < this.particles.length; i++) { var particle = this.particles[i]; particle.update(timeDelta); - if (particle.position.y - particle.size > this.canvas.height) { + if ( + particle.position.y - particle.size > + this.canvas.height - this.floor + ) { // reset the particle to the top and a random x position particle.position.y = -particle.size; particle.position.x = particle.origin.x = @@ -153,8 +186,10 @@ export class SnowEffect implements Effect { private draw() { if (!this.ctx) { + console.log('Canvas context not initialized'); return; } + // TODO: Vary the alpha based on the size of the particle this.ctx.fillStyle = 'rgb(255,255,255)'; for (var i = 0; i < this.particles.length; i++) { @@ -170,12 +205,13 @@ export class SnowEffect implements Effect { private clear() { if (!this.ctx || !this.canvas) { + console.log('Canvas or context not initialized'); return; } this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } private queue() { - window.requestAnimationFrame(this.loop); + window.requestAnimationFrame(() => this.loop()); } } diff --git a/src/panel/main.ts b/src/panel/main.ts index 5a60b551..17760182 100644 --- a/src/panel/main.ts +++ b/src/panel/main.ts @@ -19,7 +19,15 @@ import { } from './pets'; import { PetElementState, PetPanelState } from './states'; import { THEMES } from './themes'; -import { dynamicThrowOff, dynamicThrowOn, setupBallThrowing, throwAndChase } from './ball'; +import { + dynamicThrowOff, + dynamicThrowOn, + setupBallThrowing, + throwAndChase, +} from './ball'; + +const EFFECT_CANVAS_ID = 'effectCanvas'; +const PET_CANVAS_ID = 'ballCanvas'; /* This is how the VS Code API can be invoked from the panel */ declare global { @@ -249,6 +257,7 @@ export function petPanelApp( petSize: PetSize, petType: PetType, throwBallWithMouse: boolean, + disableEffects: boolean, stateApi?: VscodeStateApi, ) { if (!stateApi) { @@ -257,9 +266,17 @@ export function petPanelApp( const themeInfo = THEMES[theme]; // Apply Theme backgrounds const foregroundEl = document.getElementById('foreground'); - document.body.style.backgroundImage = themeInfo.backgroundImageUrl(basePetUri, themeKind, petSize); + document.body.style.backgroundImage = themeInfo.backgroundImageUrl( + basePetUri, + themeKind, + petSize, + ); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - foregroundEl!.style.backgroundImage = themeInfo.foregroundImageUrl(basePetUri, themeKind, petSize); + foregroundEl!.style.backgroundImage = themeInfo.foregroundImageUrl( + basePetUri, + themeKind, + petSize, + ); const floor = themeInfo.floor(petSize); console.log( @@ -268,6 +285,7 @@ export function petPanelApp( basePetUri, petType, throwBallWithMouse, + theme, ); // New session @@ -294,8 +312,8 @@ export function petPanelApp( recoverState(basePetUri, petSize, floor, stateApi); } - initCanvas('petCanvas'); - setupBallThrowing('petCanvas', petSize, floor); + initCanvas(PET_CANVAS_ID); + setupBallThrowing(PET_CANVAS_ID, petSize, floor); if (throwBallWithMouse) { dynamicThrowOn(allPets.pets); @@ -305,10 +323,12 @@ export function petPanelApp( // Initialize any effects if (themeInfo.effect) { - const effectCanvas = initCanvas('effectCanvas'); + const effectCanvas = initCanvas(EFFECT_CANVAS_ID); if (effectCanvas) { themeInfo.effect.init(effectCanvas, petSize, floor, themeKind); - themeInfo.effect.enable(); + if (!disableEffects) { + themeInfo.effect.enable(); + } } } @@ -394,10 +414,17 @@ export function petPanelApp( petCounter = 1; saveState(stateApi); break; + case 'disable-effects': + if (themeInfo.effect && message.disabled) { + themeInfo.effect.disable(); + } else if (themeInfo.effect && !message.disabled) { + themeInfo.effect.enable(); + } + break; } }); } window.addEventListener('resize', function () { - initCanvas('petCanvas'); - initCanvas('effectCanvas'); + initCanvas(PET_CANVAS_ID); + initCanvas(EFFECT_CANVAS_ID); }); diff --git a/src/panel/themes.ts b/src/panel/themes.ts index bdd4e1e7..dfe4401e 100644 --- a/src/panel/themes.ts +++ b/src/panel/themes.ts @@ -2,13 +2,15 @@ import { ColorThemeKind, PetSize, Theme } from '../common/types'; import { Effect } from './effects/effect'; import { SnowEffect } from './effects/snow'; -function normalizeColorThemeKind(kind: ColorThemeKind): string { +function normalizeColorThemeKind(kind: ColorThemeKind): 'dark' | 'light' { switch (kind) { case ColorThemeKind.light: return 'light'; case ColorThemeKind.dark: return 'dark'; case ColorThemeKind.highContrast: + return 'dark'; + case ColorThemeKind.highContrastLight: return 'light'; default: return 'light'; @@ -125,14 +127,14 @@ class WinterThemeInfo extends ThemeInfo { floor(size: PetSize): number { switch (size) { case PetSize.small: - return 60; + return 20; case PetSize.medium: - return 80; + return 30; case PetSize.large: - return 120; + return 45; case PetSize.nano: default: - return 45; + return 18; } } } @@ -142,20 +144,19 @@ export const THEMES: Record = { none: { name: 'none', description: 'No theme', - // eslint-disable-next-line no-unused-vars + /* eslint-disable no-unused-vars */ floor: (size: PetSize) => 0, - // eslint-disable-next-line no-unused-vars backgroundImageUrl: ( basePetUri: string, themeKind: ColorThemeKind, petSize: PetSize, ) => '', - // eslint-disable-next-line no-unused-vars foregroundImageUrl: ( basePetUri: string, themeKind: ColorThemeKind, petSize: PetSize, ) => '', + /* eslint-enable no-unused-vars */ }, forest: new ForestThemeInfo(), castle: new CastleThemeInfo(), diff --git a/src/test/suite/panel.test.ts b/src/test/suite/panel.test.ts index d635c348..03d78b3e 100644 --- a/src/test/suite/panel.test.ts +++ b/src/test/suite/panel.test.ts @@ -17,7 +17,7 @@ import * as pets from '../../panel/pets'; function mockPanelWindow() { const html = - '
'; + '
'; var jsdom = require('jsdom'); var document = new jsdom.JSDOM(html); @@ -133,6 +133,7 @@ suite('Pets Test Suite', () => { PetSize.large, petType, false, + false, mockState, ); @@ -179,6 +180,7 @@ suite('Pets Test Suite', () => { PetSize.large, PetType.cat, false, + false, mockState, ); @@ -201,6 +203,7 @@ suite('Pets Test Suite', () => { PetSize.large, PetType.cat, false, + false, mockState, );