快乐的梦鱼 3 tygodni temu
rodzic
commit
c4e5bdc768

+ 0 - 7
package-lock.json

@@ -27,7 +27,6 @@
         "@imengyu/imengyu-utils": "^0.0.27",
         "@imengyu/js-request-transform": "^0.4.0",
         "async-validator": "^4.2.5",
-        "cax": "^1.3.6",
         "crypto-js": "^4.2.0",
         "pinia": "^3.0.1",
         "tslib": "^2.8.1",
@@ -9020,12 +9019,6 @@
       ],
       "license": "CC-BY-4.0"
     },
-    "node_modules/cax": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmmirror.com/cax/-/cax-1.3.6.tgz",
-      "integrity": "sha512-t/YuUc7pizVSLu4B0XnwxI3wZnhvyRpmf+VukmfzY/QqSfIcfXomaxWE9eK9Q8Cg57/IUatST5i/8yvYrA5yRQ==",
-      "license": "MIT"
-    },
     "node_modules/centra": {
       "version": "2.7.0",
       "resolved": "https://registry.npmmirror.com/centra/-/centra-2.7.0.tgz",

+ 0 - 1
package.json

@@ -54,7 +54,6 @@
     "@imengyu/imengyu-utils": "^0.0.27",
     "@imengyu/js-request-transform": "^0.4.0",
     "async-validator": "^4.2.5",
-    "cax": "^1.3.6",
     "crypto-js": "^4.2.0",
     "pinia": "^3.0.1",
     "tslib": "^2.8.1",

+ 679 - 0
src/components/canvas/MiniRender.ts

@@ -0,0 +1,679 @@
+export namespace MiniRender {
+  
+  export interface RendeCanvasInterface {
+    initCanvas(width: number, height: number): Promise<void>;
+    createImage(src: string): Promise<any>;
+    clearCanvas(): void;
+    requestAnimationFrame(draw: (ts: number) => void): number;
+    cancelAnimationFrame(id: number): void;
+    getCtx(): CanvasRenderingContext2D;
+  }
+
+  export type RenderObjectId = string;
+
+  export interface Disposable {
+    dispose(): void;
+  }
+
+  export interface RenderContext {
+    readonly scene: Scene;
+    readonly canvas: RendeCanvasInterface;
+    readonly ctx: CanvasRenderingContext2D;
+  }
+
+  export interface IRenderable {
+    render(rc: RenderContext): void;
+  }
+
+  export interface IUpdatable {
+    update(dtMs: number): void;
+  }
+
+  export interface TransformLike {
+    x: number;
+    y: number;
+    width: number;
+    height: number;
+    rotation: number; // radians
+    alpha: number; // 0..1
+    scaleX: number;
+    scaleY: number;
+    anchorX: number; // 0..1 (relative to width)
+    anchorY: number; // 0..1 (relative to height)
+  }
+
+  export type RenderObjectEventName = "added" | "removed";
+  export type RenderObjectEventHandler = (obj: RenderObject) => void;
+
+  export class RenderObject implements TransformLike, IRenderable, IUpdatable {
+    public readonly id: RenderObjectId;
+    public name?: string;
+
+    public x = 0;
+    public y = 0;
+    public width = 0;
+    public height = 0;
+    public rotation = 0;
+    public alpha = 1;
+    public scaleX = 1;
+    public scaleY = 1;
+    public anchorX = 0;
+    public anchorY = 0;
+
+    public visible = true;
+    public zIndex = 0;
+
+    public parent: Container | null = null;
+
+    private listeners = new Map<RenderObjectEventName, Set<RenderObjectEventHandler>>();
+
+    constructor(id?: RenderObjectId) {
+      this.id = id ?? RenderObject.newId();
+    }
+
+    protected static newId(): RenderObjectId {
+      return `ro_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
+    }
+
+    public on(event: RenderObjectEventName, handler: RenderObjectEventHandler): () => void {
+      const set = this.listeners.get(event) ?? new Set<RenderObjectEventHandler>();
+      set.add(handler);
+      this.listeners.set(event, set);
+      return () => this.off(event, handler);
+    }
+
+    public off(event: RenderObjectEventName, handler: RenderObjectEventHandler): void {
+      this.listeners.get(event)?.delete(handler);
+    }
+
+    public emit(event: RenderObjectEventName): void {
+      const set = this.listeners.get(event);
+      if (!set) return;
+      for (const h of set) h(this);
+    }
+
+    protected withTransform(rc: RenderContext, draw: () => void): void {
+      const { ctx } = rc;
+      ctx.save();
+
+      const ax = this.anchorX * this.width;
+      const ay = this.anchorY * this.height;
+
+      ctx.translate(this.x + ax, this.y + ay);
+      if (this.rotation) ctx.rotate(this.rotation);
+      if (this.scaleX !== 1 || this.scaleY !== 1) ctx.scale(this.scaleX, this.scaleY);
+
+      const prevAlpha = ctx.globalAlpha;
+      ctx.globalAlpha = (prevAlpha ?? 1) * this.alpha;
+
+      ctx.translate(-ax, -ay);
+      draw();
+      ctx.restore();
+    }
+
+    protected draw(_rc: RenderContext): void {}
+
+    public render(rc: RenderContext): void {
+      if (!this.visible || this.alpha <= 0) return;
+      this.withTransform(rc, () => this.draw(rc));
+    }
+
+    public update(_dtMs: number): void {}
+  }
+
+  export class Container extends RenderObject {
+    private _children: RenderObject[] = [];
+
+    public get children(): readonly RenderObject[] {
+      return this._children;
+    }
+
+    public add(child: RenderObject): this {
+      if (child.parent) child.parent.remove(child);
+      child.parent = this;
+      this._children.push(child);
+      this.sortChildren();
+      child.emit("added");
+      return this;
+    }
+
+    public remove(child: RenderObject): this {
+      const idx = this._children.indexOf(child);
+      if (idx < 0) return this;
+      this._children.splice(idx, 1);
+      child.parent = null;
+      child.emit("removed");
+      return this;
+    }
+
+    public removeAll(): this {
+      for (const c of this._children) {
+        c.parent = null;
+        c.emit("removed");
+      }
+      this._children = [];
+      return this;
+    }
+
+    public sortChildren(): void {
+      this._children.sort((a, b) => a.zIndex - b.zIndex);
+    }
+
+    public override update(dtMs: number): void {
+      for (const c of this._children) {
+        (c as unknown as IUpdatable).update?.(dtMs);
+      }
+    }
+
+    protected override draw(rc: RenderContext): void {
+      for (const c of this._children) c.render(rc);
+    }
+  }
+
+  export class Rect extends RenderObject {
+    public fillStyle: string | null = "#000000";
+    public strokeStyle: string | null = null;
+    public lineWidth = 1;
+    public radius = 0;
+
+    protected override draw(rc: RenderContext): void {
+      const { ctx } = rc;
+      const w = this.width;
+      const h = this.height;
+      if (w <= 0 || h <= 0) return;
+
+      if (this.radius > 0) {
+        const r = Math.max(0, Math.min(this.radius, Math.min(w, h) / 2));
+        ctx.beginPath();
+        ctx.moveTo(r, 0);
+        ctx.lineTo(w - r, 0);
+        ctx.arcTo(w, 0, w, r, r);
+        ctx.lineTo(w, h - r);
+        ctx.arcTo(w, h, w - r, h, r);
+        ctx.lineTo(r, h);
+        ctx.arcTo(0, h, 0, h - r, r);
+        ctx.lineTo(0, r);
+        ctx.arcTo(0, 0, r, 0, r);
+        ctx.closePath();
+
+        if (this.fillStyle) {
+          ctx.fillStyle = this.fillStyle;
+          ctx.fill();
+        }
+        if (this.strokeStyle) {
+          ctx.strokeStyle = this.strokeStyle;
+          ctx.lineWidth = this.lineWidth;
+          ctx.stroke();
+        }
+        return;
+      }
+
+      if (this.fillStyle) {
+        ctx.fillStyle = this.fillStyle;
+        ctx.fillRect(0, 0, w, h);
+      }
+      if (this.strokeStyle) {
+        ctx.strokeStyle = this.strokeStyle;
+        ctx.lineWidth = this.lineWidth;
+        ctx.strokeRect(0, 0, w, h);
+      }
+    }
+  }
+
+  export class Sprite extends RenderObject {
+    public src: string = "";
+    public image: any | null = null;
+
+    protected override draw(rc: RenderContext): void {
+      const { ctx, scene } = rc;
+      if (!this.src) return;
+      const w = this.width;
+      const h = this.height;
+      if (w <= 0 || h <= 0) return;
+
+      const img = this.image ?? scene.assets.getImageSync(this.src);
+      if (!img) {
+        scene.assets
+          .getImage(this.src)
+          .then((loaded) => {
+            this.image = loaded;
+          })
+          .catch(() => {});
+        return;
+      }
+      ctx.drawImage(img, 0, 0, w, h);
+    }
+  }
+
+  export type TextAlign = CanvasTextAlign;
+  export type TextBaseline = CanvasTextBaseline;
+
+  export interface TextStyle {
+    fontSize?: number;
+    fontFamily?: string;
+    fontWeight?: string | number;
+    fontStyle?: string;
+    color?: string;
+    strokeColor?: string | null;
+    strokeWidth?: number;
+    align?: TextAlign;
+    baseline?: TextBaseline;
+    lineHeight?: number;
+    /**
+     * - "nowrap": 单行(遇到 \n 也会分行)
+     * - "wrap": 自动按 maxWidth/width 换行
+     */
+    wrap?: "nowrap" | "wrap";
+    /** 指定自动换行宽度;若不设置则使用 `width` */
+    maxWidth?: number;
+  }
+
+  export class Text extends RenderObject {
+    public text = "";
+    public style: Required<TextStyle> = {
+      fontSize: 16,
+      fontFamily: "sans-serif",
+      fontWeight: "normal",
+      fontStyle: "normal",
+      color: "#000000",
+      strokeColor: null,
+      strokeWidth: 1,
+      align: "left",
+      baseline: "top",
+      lineHeight: 20,
+      wrap: "nowrap",
+      maxWidth: 0,
+    };
+
+    constructor(text?: string, style?: TextStyle) {
+      super();
+      if (typeof text === "string") this.text = text;
+      if (style) this.setStyle(style);
+    }
+
+    public setStyle(style: TextStyle): void {
+      this.style = { ...this.style, ...style };
+      if (!style.lineHeight && style.fontSize) {
+        // 常见经验值
+        this.style.lineHeight = Math.round(style.fontSize * 1.25);
+      }
+    }
+
+    private applyTextStyle(ctx: CanvasRenderingContext2D): void {
+      const s = this.style;
+      ctx.font = `${s.fontStyle} ${s.fontWeight} ${s.fontSize}px ${s.fontFamily}`.trim();
+      ctx.fillStyle = s.color;
+      ctx.textAlign = s.align;
+      ctx.textBaseline = s.baseline;
+      if (s.strokeColor) {
+        ctx.strokeStyle = s.strokeColor;
+        ctx.lineWidth = s.strokeWidth;
+      }
+    }
+
+    private splitLines(ctx: CanvasRenderingContext2D): string[] {
+      const raw = (this.text ?? "").toString();
+      const baseLines = raw.split(/\r?\n/);
+
+      if (this.style.wrap !== "wrap") return baseLines;
+
+      const max =
+        (this.style.maxWidth > 0 ? this.style.maxWidth : 0) ||
+        (this.width > 0 ? this.width : 0);
+      if (max <= 0) return baseLines;
+
+      const out: string[] = [];
+      for (const line of baseLines) {
+        if (!line) {
+          out.push("");
+          continue;
+        }
+
+        let current = "";
+        for (const ch of [...line]) {
+          const test = current + ch;
+          const w = ctx.measureText(test).width;
+          if (w > max && current) {
+            out.push(current);
+            current = ch;
+          } else {
+            current = test;
+          }
+        }
+        out.push(current);
+      }
+      return out;
+    }
+
+    private computeAutoSize(ctx: CanvasRenderingContext2D, lines: string[]): void {
+      // 未指定 width 时,使用最长行宽;指定了 maxWidth 时以 maxWidth 为上限
+      if (this.width <= 0) {
+        let maxLine = 0;
+        for (const l of lines) maxLine = Math.max(maxLine, ctx.measureText(l).width);
+        const limit = this.style.maxWidth > 0 ? this.style.maxWidth : 0;
+        this.width = limit > 0 ? Math.min(limit, maxLine) : maxLine;
+      }
+      if (this.height <= 0) {
+        this.height = lines.length * this.style.lineHeight;
+      }
+    }
+
+    protected override draw(rc: RenderContext): void {
+      const { ctx } = rc;
+      if (!this.text) return;
+
+      this.applyTextStyle(ctx);
+
+      const lines = this.splitLines(ctx);
+      this.computeAutoSize(ctx, lines);
+
+      const s = this.style;
+
+      // 对齐偏移:以对象局部坐标系 (0,0) 为绘制起点
+      const xBase =
+        s.align === "center" ? this.width / 2 : s.align === "right" || s.align === "end" ? this.width : 0;
+
+      let y = 0;
+      for (const line of lines) {
+        if (s.strokeColor) ctx.strokeText(line, xBase, y, s.maxWidth > 0 ? s.maxWidth : undefined);
+        ctx.fillText(line, xBase, y, s.maxWidth > 0 ? s.maxWidth : undefined);
+        y += s.lineHeight;
+      }
+    }
+  }
+
+  export type AnimateSpriteFrame = [
+    x: number,
+    y: number,
+    width: number,
+    height: number,
+    originX?: number,
+    originY?: number,
+    imageIndex?: number,
+  ];
+
+  export interface AnimateSpriteAnimation {
+    /** 指向 `frames` 的索引数组 */
+    frames: number[];
+    /** 播放速度倍率(1 = 使用 framerate) */
+    speed?: number;
+    /** 是否循环覆盖全局 playOnce */
+    loop?: boolean;
+  }
+
+  export interface AnimateSpriteConfig {
+    framerate: number;
+    images: string[];
+    frames: AnimateSpriteFrame[];
+    animations: Record<string, AnimateSpriteAnimation>;
+    playOnce?: boolean;
+    currentAnimation: string;
+  }
+
+  export class AnimateSprite extends RenderObject {
+    public framerate = 7;
+    public images: string[] = [];
+    public frames: AnimateSpriteFrame[] = [];
+    public animations: Record<string, AnimateSpriteAnimation> = {};
+    public playOnce = false;
+    public currentAnimation = "";
+
+    public playing = true;
+
+    private _frameCursor = 0;
+    private _accMs = 0;
+    private _resolvedImages: Array<any | null> = [];
+
+    constructor(config?: Partial<AnimateSpriteConfig>) {
+      super();
+      if (config) Object.assign(this, config);
+      this._resolvedImages = new Array(this.images.length).fill(null);
+      if (this.currentAnimation && !this.animations[this.currentAnimation]) {
+        // 若未配置 animations,则允许把 currentAnimation 当成 “默认帧序列” 的别名
+        this.animations[this.currentAnimation] = { frames: [0] };
+      }
+    }
+
+    public play(name?: string, opts?: { reset?: boolean; once?: boolean }): void {
+      if (name) this.currentAnimation = name;
+      if (opts?.reset ?? true) {
+        this._frameCursor = 0;
+        this._accMs = 0;
+      }
+      if (typeof opts?.once === "boolean") this.playOnce = opts.once;
+      this.playing = true;
+    }
+
+    public stop(): void {
+      this.playing = false;
+    }
+
+    public gotoAndStop(frameCursor: number): void {
+      this._frameCursor = Math.max(0, frameCursor | 0);
+      this._accMs = 0;
+      this.playing = false;
+    }
+
+    public get currentFrameIndex(): number {
+      const seq = this.getAnimationSequence();
+      if (seq.length <= 0) return 0;
+      const cursor = Math.max(0, Math.min(this._frameCursor, seq.length - 1));
+      return seq[cursor] ?? 0;
+    }
+
+    private getAnimationSequence(): number[] {
+      const anim = this.animations[this.currentAnimation];
+      if (anim?.frames?.length) return anim.frames;
+      // fallback:没有动画就按全部 frames 依次播放
+      return this.frames.map((_, i) => i);
+    }
+
+    private getEffectiveFrameDurationMs(): number {
+      const fps = Math.max(0.0001, this.framerate);
+      const base = 1000 / fps;
+      const anim = this.animations[this.currentAnimation];
+      const speed = anim?.speed ?? 1;
+      return base / Math.max(0.0001, speed);
+    }
+
+    public override update(dtMs: number): void {
+      if (!this.playing) return;
+
+      const seq = this.getAnimationSequence();
+      if (seq.length <= 1) return;
+
+      this._accMs += dtMs;
+      const frameDur = this.getEffectiveFrameDurationMs();
+      if (this._accMs < frameDur) return;
+
+      const steps = Math.floor(this._accMs / frameDur);
+      this._accMs = this._accMs % frameDur;
+
+      this._frameCursor += steps;
+
+      const anim = this.animations[this.currentAnimation];
+      const loop = anim?.loop ?? !this.playOnce;
+      if (loop) {
+        this._frameCursor = this._frameCursor % seq.length;
+      } else {
+        if (this._frameCursor >= seq.length - 1) {
+          this._frameCursor = seq.length - 1;
+          this.playing = false;
+        }
+      }
+    }
+
+    protected override draw(rc: RenderContext): void {
+      const { ctx, scene } = rc;
+      if (!this.frames.length) return;
+
+      const fIdx = this.currentFrameIndex;
+      const frame = this.frames[fIdx];
+      if (!frame) return;
+
+      const [sx, sy, sw, sh, originX = 0, originY = 0, imageIndex = 0] = frame;
+      if (sw <= 0 || sh <= 0) return;
+
+      const src = this.images[imageIndex] ?? this.images[0];
+      if (!src) return;
+
+      // 默认使用帧尺寸作为显示尺寸
+      if (this.width <= 0) this.width = sw;
+      if (this.height <= 0) this.height = sh;
+
+      let img = this._resolvedImages[imageIndex] ?? null;
+      img = img ?? scene.assets.getImageSync(src);
+      if (!img) {
+        scene.assets
+          .getImage(src)
+          .then((loaded) => {
+            this._resolvedImages[imageIndex] = loaded;
+          })
+          .catch(() => {});
+        return;
+      }
+
+      // drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
+      ctx.drawImage(img, sx, sy, sw, sh, -originX, -originY, this.width, this.height);
+    }
+  }
+
+  export class AssetManager {
+    private imageCache = new Map<string, Promise<any>>();
+    private imageResolved = new Map<string, any>();
+
+    constructor(private canvas: RendeCanvasInterface) {}
+
+    public getImage(src: string): Promise<any> {
+      if (this.imageResolved.has(src)) return Promise.resolve(this.imageResolved.get(src));
+      const inflight = this.imageCache.get(src);
+      if (inflight) return inflight;
+
+      const p = this.canvas.createImage(src).then((img) => {
+        this.imageResolved.set(src, img);
+        return img;
+      });
+      this.imageCache.set(src, p);
+      return p;
+    }
+
+    public getImageSync(src: string): any | null {
+      return this.imageResolved.get(src) ?? null;
+    }
+
+    public clear(): void {
+      this.imageCache.clear();
+      this.imageResolved.clear();
+    }
+  }
+
+  export type CanvasAdapterFactory = (...args: any[]) => RendeCanvasInterface;
+
+  export class CanvasAdapterRegistry {
+    private static registry = new Map<string, CanvasAdapterFactory>();
+
+    public static register(name: string, factory: CanvasAdapterFactory): void {
+      this.registry.set(name, factory);
+    }
+
+    public static get(name: string): CanvasAdapterFactory | undefined {
+      return this.registry.get(name);
+    }
+
+    public static create(name: string, ...args: any[]): RendeCanvasInterface {
+      const f = this.registry.get(name);
+      if (!f) throw new Error(`CanvasAdapter 未注册: ${name}`);
+      return f(...args);
+    }
+
+    public static has(name: string): boolean {
+      return this.registry.has(name);
+    }
+
+    public static names(): string[] {
+      return [...this.registry.keys()];
+    }
+  }
+
+  export class Scene implements Disposable {
+    public readonly root: Container = new Container("root");
+    public readonly assets: AssetManager;
+
+    private rafId: number | null = null;
+    private lastTs: number | null = null;
+    public readonly canvas: RendeCanvasInterface;
+    public readonly width: number;
+    public readonly height: number;
+    public readonly backgroundColor: string;
+
+    constructor(
+      public options: {
+        canvas: RendeCanvasInterface,
+        width: number,
+        height: number,
+        backgroundColor: string,
+      },
+      public onInit: () => void,
+      public updateFn: (dtMs: number) => void,
+    ) {
+      this.canvas = options.canvas;
+      this.width = options.width;
+      this.height = options.height;
+      this.backgroundColor = options.backgroundColor;
+      this.assets = new AssetManager(this.canvas);
+    }
+
+    public async init() {
+      console.log(`[Canvas] init canvas ${this.width}x${this.height}`);
+      
+      await this.canvas.initCanvas(this.width, this.height);
+      this.onInit();
+    }
+
+    public clear(): void {
+      this.canvas.clearCanvas();
+      this.canvas.getCtx().fillStyle = this.backgroundColor;
+      this.canvas.getCtx().fillRect(0, 0, this.width, this.height);
+    }
+
+    public renderOnce(): void {
+      this.clear();
+      const rc: RenderContext = {
+        scene: this,
+        canvas: this.canvas,
+        ctx: this.canvas.getCtx(),
+      };
+      this.root.render(rc);
+    }
+
+    public tick(ts: number): void {
+      const dt = this.lastTs === null ? 16 : Math.max(0, ts - this.lastTs);
+      this.lastTs = ts;
+      this.updateFn(dt);
+      this.root.update(dt);
+      this.renderOnce();
+    }
+
+    public start(): void {
+      if (this.rafId !== null) return;
+      const loop = (ts: number) => {
+        this.tick(ts);
+        this.rafId = this.canvas.requestAnimationFrame(loop);
+      };
+      this.rafId = this.canvas.requestAnimationFrame(loop);
+    }
+
+    public stop(): void {
+      if (this.rafId === null) return;
+      this.canvas.cancelAnimationFrame(this.rafId);
+      this.rafId = null;
+      this.lastTs = null;
+    }
+
+    public dispose(): void {
+      this.stop();
+      this.assets.clear();
+      this.root.removeAll();
+    }
+  }
+}

+ 54 - 0
src/components/canvas/UniWeappRender.ts

@@ -0,0 +1,54 @@
+import { requireNotNull } from "@imengyu/imengyu-utils";
+import { MiniRender } from "./MiniRender";
+
+export class UniWeappRender implements MiniRender.RendeCanvasInterface {
+
+  constructor(private id: string, private componentInstance: any) {
+  }
+
+  private ctx: CanvasRenderingContext2D | null = null;
+  private width: number = 0;
+  private height: number = 0;
+  private canvas: any;
+
+  async initCanvas(width: number, height: number): Promise<void> {
+    this.width = width;
+    this.height = height;
+    await new Promise<void>((resolve) => {
+      uni.createSelectorQuery()
+        .in(this.componentInstance)
+        .select('#' + this.id) // 在 WXML 中填入的 id
+        .fields({ node: true }, (res) => {})
+        .exec((res) => {
+          const canvas = (res as any)[0].node
+          canvas.width = width
+          canvas.height = height
+          this.canvas = canvas
+          this.ctx = canvas.getContext('2d')
+          resolve()
+        });
+    })
+  }
+  createImage(src: string) {
+    return new Promise((resolve, reject) => {
+      const image = this.canvas.createImage()
+      image.onload = () => { resolve(image) }
+      image.onerror = () => { reject(new Error('Failed to load image')) }
+      image.src = src
+    })
+  }
+  clearCanvas(): void {
+    requireNotNull(this.ctx).clearRect(0, 0, this.width, this.height)
+  }
+  requestAnimationFrame(draw: (ts: number) => void): number {
+    return this.canvas.requestAnimationFrame(draw)
+  }
+  cancelAnimationFrame(id: number): void {
+    this.canvas.cancelAnimationFrame(id)
+  }
+  getCtx(): CanvasRenderingContext2D {
+    return requireNotNull(this.ctx)
+  }
+
+
+}

+ 7 - 0
src/pages.json

@@ -229,6 +229,13 @@
       }
     },
     {
+      "path": "pages/test/render",
+      "style": {
+        "navigationBarTitleText": "测试渲染页",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
       "path": "pages/test/blank",
       "style": {
         "navigationBarTitleText": "测试空白页",

+ 96 - 32
src/pages/home/village/components/VillageTree.vue

@@ -1,44 +1,108 @@
 <template>
   <canvas 
     id="villageTree"
-    :width="systemInfo.windowWidth" 
-    :height="200" 
+    type="2d"
+    :width="`${systemInfo.windowWidth}px`" 
+    :height="`${HEIGHT}px`" 
+    :style="{
+      width: systemInfo.windowWidth + 'px',
+      height: HEIGHT + 'px',
+    }"
   />
 </template>
 
-<script setup>
-import { defineComponent, getCurrentInstance, onMounted } from 'vue';
+<script lang="ts" setup>
+import { MiniRender } from '@/components/canvas/MiniRender';
+import { UniWeappRender } from '@/components/canvas/UniWeappRender';
+import { getCurrentInstance, onBeforeUnmount, onMounted } from 'vue';
 
+const HEIGHT = 260;
 const instance = getCurrentInstance();
 const systemInfo = uni.getSystemInfoSync();
+const render = new MiniRender.Scene(
+  {
+    canvas: new UniWeappRender('villageTree', instance),
+    width: systemInfo.windowWidth,
+    height: HEIGHT,
+    backgroundColor: '#000',
+  }, 
+  async () => {
+    const bg = new MiniRender.Sprite();
+    bg.src = 'https://xy.wenlvti.net/app_static/images/village/TreeTestBg.jpg';
+    bg.x = 0;
+    bg.y = 0;
+    bg.width = systemInfo.windowWidth;
+    bg.height = HEIGHT;
 
-let canvas = null;
-let ctx = null
-
-async function initCanvas() {
-  uni.createSelectorQuery()
-    .in(instance)
-    .select('#villageTree')
-    .fields({ node: true, size: true }, (res) => {
-      const canvas = res[0].node
-      const ctx = canvas.getContext('2d')
-     
-      // 保存到 this,方便其他函数使用
-      canvas = canvas
-      ctx = ctx
-    })
-}
-function animate() {
-}
-
-
-
-function animate() {
-  flag.rotation += 0.01
-  requestAnimationFrame(() => animate(stage, flag))
-}
-
-onMounted(() => {
-  initCanvas()
+
+    const text = new MiniRender.Text('动画问题解决啦,就等设计师出图了!', {
+      fontSize: 20,
+      color: '#000',
+      align: 'left',
+      wrap: 'wrap',
+      maxWidth: 200,
+    });
+    text.x = 70;
+    text.y = 50;
+
+    const robot = new MiniRender.AnimateSprite({
+      framerate: 7,
+      images: ['https://docs.imengyu.top/assets/character_robot_sheet.png'],
+      frames: [
+        // 来自 character_robot_sheet.xml(96x128)
+        // walk0..walk7
+        [0, 512, 96, 128],
+        [96, 512, 96, 128],
+        [192, 512, 96, 128],
+        [288, 512, 96, 128],
+        [384, 512, 96, 128],
+        [480, 512, 96, 128],
+        [576, 512, 96, 128],
+        [672, 512, 96, 128],
+        // run0..run2
+        [576, 256, 96, 128],
+        [672, 256, 96, 128],
+        [768, 256, 96, 128],
+        // attack0..attack2
+        [0, 384, 96, 128],
+        [96, 384, 96, 128],
+        [192, 384, 96, 128],
+      ],
+      animations: {
+        walk: { frames: [0, 1, 2, 3, 4, 5, 6, 7] },
+        run: { frames: [8, 9, 10] },
+        attack: { frames: [11, 12, 13], loop: false },
+      },
+      playOnce: false,
+      currentAnimation: 'walk',
+    });
+    robot.x = 30;
+    robot.y = 105;
+    robot.play();
+
+    const flag = new MiniRender.Sprite();
+    flag.src = 'https://xy.wenlvti.net/app_static/images/village/TreeTestFlag.png';
+    flag.x = 125;
+    flag.y = 144;
+    flag.width = 80;
+    flag.height = 100;
+
+    render.root
+      .add(bg)
+      .add(text)
+      .add(flag)
+      .add(robot);
+  },
+  (dtMs: number) => {
+
+  }
+);
+
+onBeforeUnmount(() => {
+  render.dispose();
+});
+onMounted(async() => {
+  await render.init()
+  render.start();  
 });
 </script>

+ 1 - 1
src/pages/home/village/index.vue

@@ -1,6 +1,6 @@
 <template>
   <FlexCol gap="gap.md">
-    <FlexRow center :padding="30" gap="gap.md">
+    <FlexRow center :padding="[0,30]" gap="gap.md">
       <HomeLargeTitle title="村社名片" :active="tab === 'card'" @click="tab = 'card'" />
       <HomeLargeTitle title="乡源树" :active="tab === 'tree'" @click="tab = 'tree'">
         <template #icon>

+ 1 - 2
src/pages/home/village/introd/tree.vue

@@ -1,10 +1,9 @@
 <template>
   <FlexCol>
-    <FlexCol :margin="[20,0]">
+    <FlexCol :margin="[10,0,0,0]">
       <Text textAlign="center" text="一人添果,全村增光;乡源树茂,故土名扬" fontConfig="primaryTitle" fontSize="35rpx"  />
     </FlexCol>
     <VillageTree />
-
     <FlexCol :padding="30">
       <FlexCol>
         <FlexRow center>

+ 110 - 0
src/pages/test/render.vue

@@ -0,0 +1,110 @@
+<template>
+  <div>
+    <canvas 
+      id="villageTree"
+      type="2d"
+      :width="systemInfo.windowWidth" 
+      :height="HEIGHT" 
+      :style="{
+        width: systemInfo.windowWidth + 'px',
+        height: HEIGHT + 'px',
+      }"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { MiniRender } from '@/components/canvas/MiniRender';
+import { UniWeappRender } from '@/components/canvas/UniWeappRender';
+import { getCurrentInstance, onBeforeUnmount, onMounted } from 'vue';
+
+const HEIGHT = 250;
+const instance = getCurrentInstance();
+const systemInfo = uni.getSystemInfoSync();
+const render = new MiniRender.Scene(
+  {
+    canvas: new UniWeappRender('villageTree', instance),
+    width: systemInfo.windowWidth,
+    height: HEIGHT,
+    backgroundColor: '#000',
+  }, 
+  async () => {
+    const bg = new MiniRender.Sprite();
+    bg.src = 'https://xy.wenlvti.net/app_static/images/village/TreeTestBg.jpg';
+    bg.x = 0;
+    bg.y = 0;
+    bg.width = systemInfo.windowWidth;
+    bg.height = HEIGHT;
+
+    const flag = new MiniRender.Sprite();
+    flag.src = 'https://xy.wenlvti.net/app_static/images/village/TreeTestFlag.png';
+    flag.x = 130;
+    flag.y = 54;
+    flag.width = 80;
+    flag.height = 100;
+
+    const text = new MiniRender.Text('动画问题解决啦,就等设计师出图了!', {
+      fontSize: 20,
+      color: '#000',
+      align: 'left',
+      wrap: 'wrap',
+      maxWidth: 200,
+    });
+    text.x = 20;
+    text.y = 20;
+
+    const robot = new MiniRender.AnimateSprite({
+      framerate: 7,
+      images: ['https://docs.imengyu.top/assets/character_robot_sheet.png'],
+      frames: [
+        // 来自 character_robot_sheet.xml(96x128)
+        // walk0..walk7
+        [0, 512, 96, 128],
+        [96, 512, 96, 128],
+        [192, 512, 96, 128],
+        [288, 512, 96, 128],
+        [384, 512, 96, 128],
+        [480, 512, 96, 128],
+        [576, 512, 96, 128],
+        [672, 512, 96, 128],
+        // run0..run2
+        [576, 256, 96, 128],
+        [672, 256, 96, 128],
+        [768, 256, 96, 128],
+        // attack0..attack2
+        [0, 384, 96, 128],
+        [96, 384, 96, 128],
+        [192, 384, 96, 128],
+      ],
+      animations: {
+        walk: { frames: [0, 1, 2, 3, 4, 5, 6, 7] },
+        run: { frames: [8, 9, 10] },
+        attack: { frames: [11, 12, 13], loop: false },
+      },
+      playOnce: false,
+      currentAnimation: 'walk',
+    });
+    robot.x = 40;
+    robot.y = 20;
+
+    robot.play();
+
+    render.root
+      .add(bg)
+      .add(text)
+      .add(flag)
+      .add(robot);
+  },
+  (dtMs: number) => {
+
+  }
+);
+
+onBeforeUnmount(() => {
+  render.dispose();
+});
+onMounted(async() => {
+  await render.init()
+  render.start();  
+});
+</script>